From a4f8c025027964a9978925396459d36b8337623d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 2 Jul 2024 08:11:32 +0200 Subject: [PATCH 01/45] Reorder initialization to simplify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This also avoids a warning with some compilers which do not understand that the condition to initialize the variable is the same as the condition to use it: ../app/src/scrcpy.c: In function ‘scrcpy’: ../app/src/scrcpy.c:750:13: warning: ‘src’ may be used uninitialized in this function [-Wmaybe-uninitialized] 750 | sc_frame_source_add_sink(src, &s->screen.frame_sink); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Refs 45fe6b602b4c050c5b1fba87cec7160093052af3 Refs --- app/src/scrcpy.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5e78dbf3..84f7c571 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -730,23 +730,20 @@ scrcpy(struct scrcpy_options *options) { .start_fps_counter = options->start_fps_counter, }; - struct sc_frame_source *src; - if (options->video_playback) { - src = &s->video_decoder.frame_source; - if (options->display_buffer) { - sc_delay_buffer_init(&s->display_buffer, - options->display_buffer, true); - sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); - src = &s->display_buffer.frame_source; - } - } - if (!sc_screen_init(&s->screen, &screen_params)) { goto end; } screen_initialized = true; if (options->video_playback) { + struct sc_frame_source *src = &s->video_decoder.frame_source; + if (options->display_buffer) { + sc_delay_buffer_init(&s->display_buffer, + options->display_buffer, true); + sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); + src = &s->display_buffer.frame_source; + } + sc_frame_source_add_sink(src, &s->screen.frame_sink); } } From b419eef55e19a4700af3ea0a170d8d0a9ce6bd16 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Jul 2024 08:17:33 +0200 Subject: [PATCH 02/45] Do not report error on device disconnected A device disconnection (when the adb connection is closed) makes the read() on the "receiver" socket fail. Since commit 063a8339ed27b94a8fe1e53a284507eb2d044e15, this is reported as an error. As a consequence, scrcpy fails with: ERROR: Controller error instead of: WARN: Device disconnected To fix the issue, report a device disconnection in that case. PR #5044 --- app/src/controller.c | 40 ++++++++++++++++++++++++++-------------- app/src/controller.h | 3 ++- app/src/receiver.c | 8 ++++++-- app/src/receiver.h | 2 +- app/src/scrcpy.c | 11 ++++++++--- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index edd767eb..d50e1921 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -7,12 +7,13 @@ #define SC_CONTROL_MSG_QUEUE_MAX 64 static void -sc_controller_receiver_on_error(struct sc_receiver *receiver, void *userdata) { +sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error, + void *userdata) { (void) receiver; struct sc_controller *controller = userdata; // Forward the event to the controller listener - controller->cbs->on_error(controller, controller->cbs_userdata); + controller->cbs->on_ended(controller, error, controller->cbs_userdata); } bool @@ -27,7 +28,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, } static const struct sc_receiver_callbacks receiver_cbs = { - .on_error = sc_controller_receiver_on_error, + .on_ended = sc_controller_receiver_on_ended, }; ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs, @@ -55,7 +56,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, controller->control_socket = control_socket; controller->stopped = false; - assert(cbs && cbs->on_error); + assert(cbs && cbs->on_ended); controller->cbs = cbs; controller->cbs_userdata = cbs_userdata; @@ -110,21 +111,30 @@ sc_controller_push_msg(struct sc_controller *controller, static bool process_msg(struct sc_controller *controller, - const struct sc_control_msg *msg) { + const struct sc_control_msg *msg, bool *eos) { static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; size_t length = sc_control_msg_serialize(msg, serialized_msg); if (!length) { + *eos = false; return false; } + ssize_t w = net_send_all(controller->control_socket, serialized_msg, length); - return (size_t) w == length; + if ((size_t) w != length) { + *eos = true; + return false; + } + + return true; } static int run_controller(void *data) { struct sc_controller *controller = data; + bool error = false; + for (;;) { sc_mutex_lock(&controller->mutex); while (!controller->stopped @@ -134,6 +144,7 @@ run_controller(void *data) { if (controller->stopped) { // stop immediately, do not process further msgs sc_mutex_unlock(&controller->mutex); + LOGD("Controller stopped"); break; } @@ -141,20 +152,21 @@ run_controller(void *data) { struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue); sc_mutex_unlock(&controller->mutex); - bool ok = process_msg(controller, &msg); + bool eos; + bool ok = process_msg(controller, &msg, &eos); sc_control_msg_destroy(&msg); if (!ok) { - LOGD("Could not write msg to socket"); - goto error; + if (eos) { + LOGD("Controller stopped (socket closed)"); + } // else error already logged + error = !eos; + break; } } + controller->cbs->on_ended(controller, error, controller->cbs_userdata); + return 0; - -error: - controller->cbs->on_error(controller, controller->cbs_userdata); - - return 1; // ignored } bool diff --git a/app/src/controller.h b/app/src/controller.h index 353d4d0d..57ad79b3 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -28,7 +28,8 @@ struct sc_controller { }; struct sc_controller_callbacks { - void (*on_error)(struct sc_controller *controller, void *userdata); + void (*on_ended)(struct sc_controller *controller, bool error, + void *userdata); }; bool diff --git a/app/src/receiver.c b/app/src/receiver.c index fb923ac4..3e572067 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -21,7 +21,7 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, receiver->acksync = NULL; receiver->uhid_devices = NULL; - assert(cbs && cbs->on_error); + assert(cbs && cbs->on_ended); receiver->cbs = cbs; receiver->cbs_userdata = cbs_userdata; @@ -134,12 +134,15 @@ run_receiver(void *data) { static uint8_t buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; + bool error = false; + for (;;) { assert(head < DEVICE_MSG_MAX_SIZE); ssize_t r = net_recv(receiver->control_socket, buf + head, DEVICE_MSG_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); + // device disconnected: keep error=false break; } @@ -147,6 +150,7 @@ run_receiver(void *data) { ssize_t consumed = process_msgs(receiver, buf, head); if (consumed == -1) { // an error occurred + error = true; break; } @@ -157,7 +161,7 @@ run_receiver(void *data) { } } - receiver->cbs->on_error(receiver, receiver->cbs_userdata); + receiver->cbs->on_ended(receiver, error, receiver->cbs_userdata); return 0; } diff --git a/app/src/receiver.h b/app/src/receiver.h index ef83978f..b1ae4fde 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -25,7 +25,7 @@ struct sc_receiver { }; struct sc_receiver_callbacks { - void (*on_error)(struct sc_receiver *receiver, void *userdata); + void (*on_ended)(struct sc_receiver *receiver, bool error, void *userdata); }; bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 84f7c571..376d5839 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -269,13 +269,18 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, } static void -sc_controller_on_error(struct sc_controller *controller, void *userdata) { +sc_controller_on_ended(struct sc_controller *controller, bool error, + void *userdata) { // Note: this function may be called twice, once from the controller thread // and once from the receiver thread (void) controller; (void) userdata; - PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); + if (error) { + PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); + } else { + PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + } } static void @@ -567,7 +572,7 @@ scrcpy(struct scrcpy_options *options) { if (options->control) { static const struct sc_controller_callbacks controller_cbs = { - .on_error = sc_controller_on_error, + .on_ended = sc_controller_on_ended, }; if (!sc_controller_init(&s->controller, s->server.control_socket, From 46041e0cc0d2467d470b444cb50cd9b6ae1fa695 Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 5 Jul 2024 01:38:41 +0200 Subject: [PATCH 03/45] Always initialize display->gl_context on macOS Otherwise SDL_GL_DeleteContext() tried to access an uninitialized pointer upon exit when not using the OpenGL renderer. SDL_GL_DeleteContext() doesn't try to delete a NULL pointer, so no need to check for that. Fixes #5057 PR #5058 Signed-off-by: Romain Vimont --- app/src/display.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/display.c b/app/src/display.c index 9f5fb0c6..39018834 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -43,6 +43,10 @@ sc_display_init(struct sc_display *display, SDL_Window *window, display->mipmaps = false; +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + display->gl_context = NULL; +#endif + // starts with "opengl" bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); if (use_opengl) { From b50f9eb41d1bd7b75a77e2c388b0144365e3cd5f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:58:44 +0200 Subject: [PATCH 04/45] Add workaround for Skyworth devices The vendor-modified ROM of Skyworth devices needs a valid app info/context. Fixes #4922 --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 448e7099..c9a26d78 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -63,9 +63,11 @@ public final class Workarounds { // - // - mustFillAppInfo = true; - } else if (Build.BRAND.equalsIgnoreCase("honor")) { + } else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth")) { // More workarounds must be applied for Honor devices: // - + // and Skyworth devices: + // - // // The system context must not be set for all devices, because it would cause other problems: // - From 487a6b9cf4a2ddc777a336d4b4c747ed33fa87a6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:17:11 +0200 Subject: [PATCH 05/45] Remove top-level const For consistency, never use top-level const for local variables. PR #5076 --- app/src/input_manager.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 43b10d2d..9da3a727 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -846,9 +846,9 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // can be used instead of Ctrl. The "virtual finger" has a position // inverted with respect to the vertical axis of symmetry in the middle of // the screen. - const SDL_Keymod keymod = SDL_GetModState(); - const bool ctrl_pressed = keymod & KMOD_CTRL; - const bool shift_pressed = keymod & KMOD_SHIFT; + SDL_Keymod keymod = SDL_GetModState(); + bool ctrl_pressed = keymod & KMOD_CTRL; + bool shift_pressed = keymod & KMOD_SHIFT; if (event->button == SDL_BUTTON_LEFT && ((down && !im->vfinger_down && ((ctrl_pressed && !shift_pressed) || From 6d98766cd5cf93804fd14d1eb9765ab9e1357cb1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:18:06 +0200 Subject: [PATCH 06/45] Simplify boolean condition using XOR (A && !B) || (!A && B) <==> A ^ B PR #5076 --- app/src/input_manager.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 9da3a727..96075f84 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -850,9 +850,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool ctrl_pressed = keymod & KMOD_CTRL; bool shift_pressed = keymod & KMOD_SHIFT; if (event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && - ((ctrl_pressed && !shift_pressed) || - (!ctrl_pressed && shift_pressed))) || + ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || (!down && im->vfinger_down))) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, From 0bce4d7f56a4d109d450c4e41e4387a086c820ef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:29:47 +0200 Subject: [PATCH 07/45] Add missing SC_ prefix for pointer id constants PR #5076 --- app/src/control_msg.c | 8 ++++---- app/src/control_msg.h | 8 ++++---- app/src/input_manager.c | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index b3da5fe5..5a800040 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -64,13 +64,13 @@ static const char *const copy_key_labels[] = { static inline const char * get_well_known_pointer_id_name(uint64_t pointer_id) { switch (pointer_id) { - case POINTER_ID_MOUSE: + case SC_POINTER_ID_MOUSE: return "mouse"; - case POINTER_ID_GENERIC_FINGER: + case SC_POINTER_ID_GENERIC_FINGER: return "finger"; - case POINTER_ID_VIRTUAL_MOUSE: + case SC_POINTER_ID_VIRTUAL_MOUSE: return "vmouse"; - case POINTER_ID_VIRTUAL_FINGER: + case SC_POINTER_ID_VIRTUAL_FINGER: return "vfinger"; default: return NULL; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index cd1340ef..2ec7b5be 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -18,12 +18,12 @@ // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes #define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14) -#define POINTER_ID_MOUSE UINT64_C(-1) -#define POINTER_ID_GENERIC_FINGER UINT64_C(-2) +#define SC_POINTER_ID_MOUSE UINT64_C(-1) +#define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2) // Used for injecting an additional virtual pointer for pinch-to-zoom -#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) -#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) +#define SC_POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) +#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 96075f84..415c1293 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -376,8 +376,8 @@ simulate_virtual_finger(struct sc_input_manager *im, msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.point = point; msg.inject_touch_event.pointer_id = - im->has_secondary_click ? POINTER_ID_VIRTUAL_MOUSE - : POINTER_ID_VIRTUAL_FINGER; + im->has_secondary_click ? SC_POINTER_ID_VIRTUAL_MOUSE + : SC_POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; @@ -662,8 +662,8 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, struct sc_mouse_motion_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), - .pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, + .pointer_id = im->has_secondary_click ? SC_POINTER_ID_MOUSE + : SC_POINTER_ID_GENERIC_FINGER, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = @@ -817,8 +817,8 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, + .pointer_id = im->has_secondary_click ? SC_POINTER_ID_MOUSE + : SC_POINTER_ID_GENERIC_FINGER, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, &im->mouse_bindings), }; From 6808288823239b0f3a76f9be377e4de82e91b35a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:38:15 +0200 Subject: [PATCH 08/45] Make pointer id independent of mouse bindings The device source (MOUSE or FINGER) to use depended on whether a secondary click was possible via mouse bindings. As a first step, always use a mouse source to break this dependency. Note that this change might cause regressions in some (unknown) cases (refs f70359f14fb13f277c65b96f43ec83aba4722457), but hopefully not. Further commits will restore a finger source in some specific use cases, but independent of secondary clicks. Refs #5055 Fixes #5067 PR #5076 --- app/src/input_manager.c | 20 +++----------------- app/src/input_manager.h | 1 - 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 415c1293..71c4434b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -52,14 +52,6 @@ is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) { || (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); } -static inline bool -mouse_bindings_has_secondary_click(const struct sc_mouse_bindings *mb) { - return mb->right_click == SC_MOUSE_BINDING_CLICK - || mb->middle_click == SC_MOUSE_BINDING_CLICK - || mb->click4 == SC_MOUSE_BINDING_CLICK - || mb->click5 == SC_MOUSE_BINDING_CLICK; -} - void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { @@ -76,8 +68,6 @@ sc_input_manager_init(struct sc_input_manager *im, im->mp = params->mp; im->mouse_bindings = params->mouse_bindings; - im->has_secondary_click = - mouse_bindings_has_secondary_click(&im->mouse_bindings); im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; @@ -375,9 +365,7 @@ simulate_virtual_finger(struct sc_input_manager *im, msg.inject_touch_event.action = action; msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.point = point; - msg.inject_touch_event.pointer_id = - im->has_secondary_click ? SC_POINTER_ID_VIRTUAL_MOUSE - : SC_POINTER_ID_VIRTUAL_FINGER; + msg.inject_touch_event.pointer_id = SC_POINTER_ID_VIRTUAL_MOUSE; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; @@ -662,8 +650,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, struct sc_mouse_motion_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), - .pointer_id = im->has_secondary_click ? SC_POINTER_ID_MOUSE - : SC_POINTER_ID_GENERIC_FINGER, + .pointer_id = SC_POINTER_ID_MOUSE, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = @@ -817,8 +804,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .pointer_id = im->has_secondary_click ? SC_POINTER_ID_MOUSE - : SC_POINTER_ID_GENERIC_FINGER, + .pointer_id = SC_POINTER_ID_MOUSE, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, &im->mouse_bindings), }; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 03c42fe6..d5a5a64d 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -23,7 +23,6 @@ struct sc_input_manager { struct sc_mouse_processor *mp; struct sc_mouse_bindings mouse_bindings; - bool has_secondary_click; bool legacy_paste; bool clipboard_autosync; From 51fee79bf50b124223523eb51c437c1267c2724a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:27:59 +0200 Subject: [PATCH 09/45] Use finger source when a pointer is simulated For pinch-to-zoom, rotation and tilt simulation, always use a finger source (instead of a mouse) for both pointers (the real one and the simulated one). A "virtual" mouse does not work on all devices (e.g. on Pixel 8). PR #5076 --- app/src/input_manager.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 71c4434b..6fbd801c 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -365,7 +365,7 @@ simulate_virtual_finger(struct sc_input_manager *im, msg.inject_touch_event.action = action; msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.point = point; - msg.inject_touch_event.pointer_id = SC_POINTER_ID_VIRTUAL_MOUSE; + msg.inject_touch_event.pointer_id = SC_POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; @@ -650,7 +650,8 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, struct sc_mouse_motion_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), - .pointer_id = SC_POINTER_ID_MOUSE, + .pointer_id = im->vfinger_down ? SC_POINTER_ID_GENERIC_FINGER + : SC_POINTER_ID_MOUSE, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = @@ -800,11 +801,20 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + SDL_Keymod keymod = SDL_GetModState(); + bool ctrl_pressed = keymod & KMOD_CTRL; + bool shift_pressed = keymod & KMOD_SHIFT; + bool change_vfinger = event->button == SDL_BUTTON_LEFT && + ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || + (!down && im->vfinger_down)); + bool use_finger = im->vfinger_down || change_vfinger; + struct sc_mouse_click_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .pointer_id = SC_POINTER_ID_MOUSE, + .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER + : SC_POINTER_ID_MOUSE, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, &im->mouse_bindings), }; @@ -832,12 +842,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // can be used instead of Ctrl. The "virtual finger" has a position // inverted with respect to the vertical axis of symmetry in the middle of // the screen. - SDL_Keymod keymod = SDL_GetModState(); - bool ctrl_pressed = keymod & KMOD_CTRL; - bool shift_pressed = keymod & KMOD_SHIFT; - if (event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || - (!down && im->vfinger_down))) { + if (change_vfinger) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); From 86b8286217b0909bf409e68ac1885ee59cc4cefc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:33:10 +0200 Subject: [PATCH 10/45] Remove unused virtual mouse PR #5076 --- app/src/control_msg.c | 2 -- app/src/control_msg.h | 3 +-- server/src/main/java/com/genymobile/scrcpy/Controller.java | 5 ++--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 5a800040..9b0fab67 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -68,8 +68,6 @@ get_well_known_pointer_id_name(uint64_t pointer_id) { return "mouse"; case SC_POINTER_ID_GENERIC_FINGER: return "finger"; - case SC_POINTER_ID_VIRTUAL_MOUSE: - return "vmouse"; case SC_POINTER_ID_VIRTUAL_FINGER: return "vfinger"; default: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 2ec7b5be..80714096 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -22,8 +22,7 @@ #define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2) // Used for injecting an additional virtual pointer for pinch-to-zoom -#define SC_POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) -#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) +#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-3) enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 87faf8ba..b7d2f93e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -22,7 +22,6 @@ public class Controller implements AsyncProcessor { // control_msg.h values of the pointerId field in inject_touch_event message private static final int POINTER_ID_MOUSE = -1; - private static final int POINTER_ID_VIRTUAL_MOUSE = -3; private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); @@ -273,8 +272,8 @@ public class Controller implements AsyncProcessor { pointer.setPressure(pressure); int source; - if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) { - // real mouse event (forced by the client when --forward-on-click) + if (pointerId == POINTER_ID_MOUSE) { + // real mouse event pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; source = InputDevice.SOURCE_MOUSE; pointer.setUp(buttons == 0); From 6baea57987a1867f2157f5c1001e77ca3cb1c6c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 9 Jul 2024 18:37:53 +0200 Subject: [PATCH 11/45] Track mouse buttons state manually The buttons state was tracked by SDL_GetMouseState(), and scrcpy applied a mask to ignore buttons used for shortcuts. Instead, track the buttons actually pressed (ignoring shortcuts) manually, to prepare the introduction of more dynamic mouse shortcuts. PR #5076 --- app/src/input_events.h | 20 +++----------------- app/src/input_manager.c | 24 +++++++++++++++++------- app/src/input_manager.h | 2 ++ app/src/usb/screen_otg.c | 8 +++----- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/app/src/input_events.h b/app/src/input_events.h index ed77bcb4..bbf4372f 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -437,25 +437,11 @@ sc_mouse_button_from_sdl(uint8_t button) { } static inline uint8_t -sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, - const struct sc_mouse_bindings *mb) { +sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { assert(buttons_state < 0x100); // fits in uint8_t - uint8_t mask = SC_MOUSE_BUTTON_LEFT; - if (!mb || mb->right_click == SC_MOUSE_BINDING_CLICK) { - mask |= SC_MOUSE_BUTTON_RIGHT; - } - if (!mb || mb->middle_click == SC_MOUSE_BINDING_CLICK) { - mask |= SC_MOUSE_BUTTON_MIDDLE; - } - if (!mb || mb->click4 == SC_MOUSE_BINDING_CLICK) { - mask |= SC_MOUSE_BUTTON_X1; - } - if (!mb || mb->click5 == SC_MOUSE_BINDING_CLICK) { - mask |= SC_MOUSE_BUTTON_X2; - } - - return buttons_state & mask; + // SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index) + return buttons_state; } #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 6fbd801c..2cc34afa 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -77,6 +77,8 @@ sc_input_manager_init(struct sc_input_manager *im, im->vfinger_invert_x = false; im->vfinger_invert_y = false; + im->mouse_buttons_state = 0; + im->last_keycode = SDLK_UNKNOWN; im->last_mod = 0; im->key_repeat = 0; @@ -654,8 +656,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, : SC_POINTER_ID_MOUSE, .xrel = event->xrel, .yrel = event->yrel, - .buttons_state = - sc_mouse_buttons_state_from_sdl(event->state, &im->mouse_bindings), + .buttons_state = im->mouse_buttons_state, }; assert(im->mp->ops->process_mouse_motion); @@ -736,6 +737,13 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool control = im->controller; bool paused = im->screen->paused; bool down = event->type == SDL_MOUSEBUTTONDOWN; + + enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button); + if (!down) { + // Mark the button as released + im->mouse_buttons_state &= ~button; + } + if (control && !paused) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; @@ -799,7 +807,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, return; } - uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + if (down) { + // Mark the button as pressed + im->mouse_buttons_state |= button; + } SDL_Keymod keymod = SDL_GetModState(); bool ctrl_pressed = keymod & KMOD_CTRL; @@ -815,8 +826,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, .button = sc_mouse_button_from_sdl(event->button), .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER : SC_POINTER_ID_MOUSE, - .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, - &im->mouse_bindings), + .buttons_state = im->mouse_buttons_state, }; assert(im->mp->ops->process_mouse_click); @@ -875,6 +885,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, int mouse_x; int mouse_y; uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y); + (void) buttons; // Actual buttons are tracked manually to ignore shortcuts struct sc_mouse_scroll_event evt = { .position = sc_input_manager_get_position(im, mouse_x, mouse_y), @@ -885,8 +896,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, .hscroll = CLAMP(event->x, -1, 1), .vscroll = CLAMP(event->y, -1, 1), #endif - .buttons_state = sc_mouse_buttons_state_from_sdl(buttons, - &im->mouse_bindings), + .buttons_state = im->mouse_buttons_state, }; im->mp->ops->process_mouse_scroll(im->mp, &evt); diff --git a/app/src/input_manager.h b/app/src/input_manager.h index d5a5a64d..88558549 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -32,6 +32,8 @@ struct sc_input_manager { bool vfinger_invert_x; bool vfinger_invert_y; + uint8_t mouse_buttons_state; // OR of enum sc_mouse_button values + // Tracks the number of identical consecutive shortcut key down events. // Not to be confused with event->repeat, which counts the number of // system-generated repeated key presses. diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 33500e0c..5c4f97f0 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -169,7 +169,7 @@ sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen, // .position not used for HID events .xrel = event->xrel, .yrel = event->yrel, - .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, NULL), + .buttons_state = sc_mouse_buttons_state_from_sdl(event->state), }; assert(mp->ops->process_mouse_motion); @@ -188,8 +188,7 @@ sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen, // .position not used for HID events .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL), + .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), }; assert(mp->ops->process_mouse_click); @@ -208,8 +207,7 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, // .position not used for HID events .hscroll = event->x, .vscroll = event->y, - .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL), + .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), }; assert(mp->ops->process_mouse_scroll); From 9989668226f100534452e0af812807562ff5212f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 9 Jul 2024 20:45:49 +0200 Subject: [PATCH 12/45] Add mouse secondary bindings Add secondary bindings (Shift+click) for mouse buttons. In addition to: --mouse-bind=xxxx It is now possible to pass a sequence of secondary bindings: --mouse-bind=xxxx:xxxx <--> <--> primary secondary bindings bindings If the second sequence is omitted, then it is the same as the first one. By default, for SDK mouse, primary bindings trigger shortcuts and secondary bindings forward all clicks. For AOA and UHID, the default bindings are reversed: all clicks are forwarded by default, whereas pressing Shift+click trigger shortcuts. --mouse-bind=bhsn:++++ # default for SDK --mouse-bind=++++:bhsn # default for AOA and UHID Refs 035d60cf5d3f4c83d48735b4cb4cd108a5b5f413 Refs f5e6b8092afd82bab402e7c2c3d00b1719f9bb57 Fixes #5055 PR #5076 --- app/scrcpy.1 | 10 +++- app/src/cli.c | 119 ++++++++++++++++++++++++++++------------ app/src/input_manager.c | 14 +++-- app/src/options.c | 16 ++++-- app/src/options.h | 7 ++- doc/mouse.md | 52 +++++++++++++----- 6 files changed, 156 insertions(+), 62 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index cf8dfa7f..1c0c0f7a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -258,10 +258,14 @@ LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse bac Also see \fB\-\-keyboard\fR. .TP -.BI "\-\-mouse\-bind " xxxx +.BI "\-\-mouse\-bind " xxxx[:xxxx] Configure bindings of secondary clicks. -The argument must be exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). +The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). + +The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held. + +If the second sequence of bindings is omitted, then it is the same as the first one. Each character must be one of the following: @@ -272,7 +276,7 @@ Each character must be one of the following: - 's': trigger shortcut APP_SWITCH - 'n': trigger shortcut "expand notification panel" -Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID. +Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID. .TP diff --git a/app/src/cli.c b/app/src/cli.c index 08a4aa3f..9dd49538 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -493,11 +493,17 @@ static const struct sc_option options[] = { { .longopt_id = OPT_MOUSE_BIND, .longopt = "mouse-bind", - .argdesc = "xxxx", + .argdesc = "xxxx[:xxxx]", .text = "Configure bindings of secondary clicks.\n" - "The argument must be exactly 4 characters, one for each " - "secondary click (in order: right click, middle click, 4th " - "click, 5th click).\n" + "The argument must be one or two sequences (separated by ':') " + "of exactly 4 characters, one for each secondary click (in " + "order: right click, middle click, 4th click, 5th click).\n" + "The first sequence defines the primary bindings, used when a " + "mouse button is pressed alone. The second sequence defines " + "the secondary bindings, used when a mouse button is pressed " + "while the Shift key is held.\n" + "If the second sequence of bindings is omitted, then it is the " + "same as the first one.\n" "Each character must be one of the following:\n" " '+': forward the click to the device\n" " '-': ignore the click\n" @@ -505,7 +511,8 @@ static const struct sc_option options[] = { " 'h': trigger shortcut HOME\n" " 's': trigger shortcut APP_SWITCH\n" " 'n': trigger shortcut \"expand notification panel\"\n" - "Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID.", + "Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA " + "and UHID.", }, { .shortopt = 'n', @@ -2095,24 +2102,46 @@ parse_mouse_binding(char c, enum sc_mouse_binding *b) { } static bool -parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) { - if (strlen(s) != 4) { - LOGE("Invalid mouse bindings: '%s' (expected exactly 4 characters from " - "{'+', '-', 'b', 'h', 's', 'n'})", s); +parse_mouse_binding_set(const char *s, struct sc_mouse_binding_set *mbs) { + assert(strlen(s) >= 4); + + if (!parse_mouse_binding(s[0], &mbs->right_click)) { + return false; + } + if (!parse_mouse_binding(s[1], &mbs->middle_click)) { + return false; + } + if (!parse_mouse_binding(s[2], &mbs->click4)) { + return false; + } + if (!parse_mouse_binding(s[3], &mbs->click5)) { return false; } - if (!parse_mouse_binding(s[0], &mb->right_click)) { + return true; +} + +static bool +parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) { + size_t len = strlen(s); + // either "xxxx" or "xxxx:xxxx" + if (len != 4 && (len != 9 || s[4] != ':')) { + LOGE("Invalid mouse bindings: '%s' (expected 'xxxx' or 'xxxx:xxxx', " + "with each 'x' being in {'+', '-', 'b', 'h', 's', 'n'})", s); return false; } - if (!parse_mouse_binding(s[1], &mb->middle_click)) { + + if (!parse_mouse_binding_set(s, &mb->pri)) { return false; } - if (!parse_mouse_binding(s[2], &mb->click4)) { - return false; - } - if (!parse_mouse_binding(s[3], &mb->click5)) { - return false; + + if (len == 9) { + if (!parse_mouse_binding_set(s + 5, &mb->sec)) { + return false; + } + } else { + // use the same bindings for Shift+click + mb->sec = mb->pri; } return true; @@ -2408,10 +2437,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGW("--forward-all-clicks is deprecated, " "use --mouse-bind=++++ instead."); opts->mouse_bindings = (struct sc_mouse_bindings) { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, + .pri = { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }, + .sec = { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }, }; break; case OPT_LEGACY_PASTE: @@ -2701,26 +2738,36 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } // If mouse bindings are not explictly set, configure default bindings - if (opts->mouse_bindings.right_click == SC_MOUSE_BINDING_AUTO) { - assert(opts->mouse_bindings.middle_click == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.click4 == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.click5 == SC_MOUSE_BINDING_AUTO); + if (opts->mouse_bindings.pri.right_click == SC_MOUSE_BINDING_AUTO) { + assert(opts->mouse_bindings.pri.middle_click == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.pri.click4 == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.pri.click5 == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.sec.right_click == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.sec.middle_click == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.sec.click4 == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.sec.click5 == SC_MOUSE_BINDING_AUTO); + + static struct sc_mouse_binding_set default_shortcuts = { + .right_click = SC_MOUSE_BINDING_BACK, + .middle_click = SC_MOUSE_BINDING_HOME, + .click4 = SC_MOUSE_BINDING_APP_SWITCH, + .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, + }; + + static struct sc_mouse_binding_set forward = { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }; // By default, forward all clicks only for UHID and AOA if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { - opts->mouse_bindings = (struct sc_mouse_bindings) { - .right_click = SC_MOUSE_BINDING_BACK, - .middle_click = SC_MOUSE_BINDING_HOME, - .click4 = SC_MOUSE_BINDING_APP_SWITCH, - .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, - }; + opts->mouse_bindings.pri = default_shortcuts; + opts->mouse_bindings.sec = forward; } else { - opts->mouse_bindings = (struct sc_mouse_bindings) { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, - }; + opts->mouse_bindings.pri = forward; + opts->mouse_bindings.sec = default_shortcuts; } } diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 2cc34afa..d3c94d03 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -708,7 +708,7 @@ sc_input_manager_process_touch(struct sc_input_manager *im, } static enum sc_mouse_binding -sc_input_manager_get_binding(const struct sc_mouse_bindings *bindings, +sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings, uint8_t sdl_button) { switch (sdl_button) { case SDL_BUTTON_LEFT: @@ -744,11 +744,18 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, im->mouse_buttons_state &= ~button; } + SDL_Keymod keymod = SDL_GetModState(); + bool ctrl_pressed = keymod & KMOD_CTRL; + bool shift_pressed = keymod & KMOD_SHIFT; + if (control && !paused) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; + struct sc_mouse_binding_set *bindings = !shift_pressed + ? &im->mouse_bindings.pri + : &im->mouse_bindings.sec; enum sc_mouse_binding binding = - sc_input_manager_get_binding(&im->mouse_bindings, event->button); + sc_input_manager_get_binding(bindings, event->button); assert(binding != SC_MOUSE_BINDING_AUTO); switch (binding) { case SC_MOUSE_BINDING_DISABLED: @@ -812,9 +819,6 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, im->mouse_buttons_state |= button; } - SDL_Keymod keymod = SDL_GetModState(); - bool ctrl_pressed = keymod & KMOD_CTRL; - bool shift_pressed = keymod & KMOD_SHIFT; bool change_vfinger = event->button == SDL_BUTTON_LEFT && ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || (!down && im->vfinger_down)); diff --git a/app/src/options.c b/app/src/options.c index 5556d1f9..5eec6427 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -24,10 +24,18 @@ const struct scrcpy_options scrcpy_options_default = { .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, .mouse_bindings = { - .right_click = SC_MOUSE_BINDING_AUTO, - .middle_click = SC_MOUSE_BINDING_AUTO, - .click4 = SC_MOUSE_BINDING_AUTO, - .click5 = SC_MOUSE_BINDING_AUTO, + .pri = { + .right_click = SC_MOUSE_BINDING_AUTO, + .middle_click = SC_MOUSE_BINDING_AUTO, + .click4 = SC_MOUSE_BINDING_AUTO, + .click5 = SC_MOUSE_BINDING_AUTO, + }, + .sec = { + .right_click = SC_MOUSE_BINDING_AUTO, + .middle_click = SC_MOUSE_BINDING_AUTO, + .click4 = SC_MOUSE_BINDING_AUTO, + .click5 = SC_MOUSE_BINDING_AUTO, + }, }, .camera_facing = SC_CAMERA_FACING_ANY, .port_range = { diff --git a/app/src/options.h b/app/src/options.h index f840a989..5ec809f0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -165,13 +165,18 @@ enum sc_mouse_binding { SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, }; -struct sc_mouse_bindings { +struct sc_mouse_binding_set { enum sc_mouse_binding right_click; enum sc_mouse_binding middle_click; enum sc_mouse_binding click4; enum sc_mouse_binding click5; }; +struct sc_mouse_bindings { + struct sc_mouse_binding_set pri; + struct sc_mouse_binding_set sec; // When Shift is pressed +}; + enum sc_key_inject_mode { // Inject special keys, letters and space as key events. // Inject numbers and punctuation as text events. diff --git a/doc/mouse.md b/doc/mouse.md index 1c62ddd0..ec4aea63 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -80,21 +80,37 @@ process like the _adb daemon_). ## Mouse bindings -By default, with SDK mouse, right-click triggers BACK (or POWER on) and -middle-click triggers HOME. In addition, the 4th click triggers APP_SWITCH and -the 5th click expands the notification panel. +By default, with SDK mouse: + - right-click triggers BACK (or POWER on) + - middle-click triggers HOME + - the 4th click triggers APP_SWITCH + - the 5th click expands the notification panel -In AOA and UHID mouse modes, all clicks are forwarded by default. +The secondary clicks may be forwarded to the device instead by pressing the +Shift key (e.g. Shift+right-click injects a right click to +the device). -The shortcuts can be configured using `--mouse-bind=xxxx` for any mouse mode. -The argument must be exactly 4 characters, one for each secondary click: +In AOA and UHID mouse modes, the default bindings are reversed: all clicks are +forwarded by default, and pressing Shift gives access to the +shortcuts (since the cursor is handled on the device side, it makes more sense +to forward all mouse buttons by default in these modes). + +The shortcuts can be configured using `--mouse-bind=xxxx:xxxx` for any mouse +mode. The argument must be one or two sequences (separated by `:`) of exactly 4 +characters, one for each secondary click: ``` ---mouse-bind=xxxx + .---- Shift + right click + SECONDARY |.--- Shift + middle click + BINDINGS ||.-- Shift + 4th click + |||.- Shift + 5th click + |||| + vvvv +--mouse-bind=xxxx:xxxx ^^^^ |||| - ||| `- 5th click - || `-- 4th click + PRIMARY ||| `- 5th click + BINDINGS || `-- 4th click | `--- middle click `---- right click ``` @@ -111,8 +127,18 @@ Each character must be one of the following: For example: ```bash -scrcpy --mouse-bind=bhsn # the default mode with SDK mouse -scrcpy --mouse-bind=++++ # forward all clicks (default for AOA/UHID) -scrcpy --mouse-bind=++bh # forward right and middle clicks, - # use 4th and 5th for BACK and HOME +scrcpy --mouse-bind=bhsn:++++ # the default mode for SDK mouse +scrcpy --mouse-bind=++++:bhsn # the default mode for AOA and UHID +scrcpy --mouse-bind=++bh:++sn # forward right and middle clicks, + # use 4th and 5th for BACK and HOME, + # use Shift+4th and Shift+5th for APP_SWITCH + # and expand notification panel +``` + +The second sequence of bindings may be omitted. In that case, it is the same as +the first one: + +```bash +scrcpy --mouse-bind=bhsn +scrcpy --mouse-bind=bhsn:bhsn # equivalent ``` From fe7494c4922b2dd9a8af18842d82af1e5ebf8897 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Jul 2024 12:19:47 +0200 Subject: [PATCH 13/45] Linearize try-catch blocks There are many possible method signatures for getPrimaryClip() and setPrimaryClip(). Avoid the nested try-catch blocks. --- .../scrcpy/wrappers/ClipboardManager.java | 127 +++++++++++------- 1 file changed, 76 insertions(+), 51 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index ed5c8d75..bdbba21d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -38,38 +38,53 @@ public final class ClipboardManager { if (getPrimaryClipMethod == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - } else { - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); - getMethodVersion = 0; - } catch (NoSuchMethodException e1) { - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); - getMethodVersion = 1; - } catch (NoSuchMethodException e2) { - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); - getMethodVersion = 2; - } catch (NoSuchMethodException e3) { - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); - getMethodVersion = 3; - } catch (NoSuchMethodException e4) { - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 4; - } catch (NoSuchMethodException e5) { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, - boolean.class); - getMethodVersion = 5; - } - } - } - } - } + return getPrimaryClipMethod; } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + getMethodVersion = 0; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); + getMethodVersion = 1; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); + getMethodVersion = 2; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); + getMethodVersion = 3; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 4; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 5; } return getPrimaryClipMethod; } @@ -78,27 +93,37 @@ public final class ClipboardManager { if (setPrimaryClipMethod == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - } else { - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); - setMethodVersion = 0; - } catch (NoSuchMethodException e1) { - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); - setMethodVersion = 1; - } catch (NoSuchMethodException e2) { - try { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); - setMethodVersion = 2; - } catch (NoSuchMethodException e3) { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); - setMethodVersion = 3; - } - } - } + return setPrimaryClipMethod; } + + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); + setMethodVersion = 0; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e1) { + // fall-through + } + + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); + setMethodVersion = 1; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e2) { + // fall-through + } + + try { + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); + setMethodVersion = 2; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e3) { + // fall-through + } + + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); + setMethodVersion = 3; } return setPrimaryClipMethod; } From 79242957a0ddba1c326f414cf5eafff6cc7b39c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Jul 2024 12:21:38 +0200 Subject: [PATCH 14/45] Add clipboard workaround for Honor device Fixes #5073 --- .../scrcpy/wrappers/ClipboardManager.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index bdbba21d..a0e3a7e1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -82,9 +82,17 @@ public final class ClipboardManager { // fall-through } - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 5; + try { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 5; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class); + getMethodVersion = 6; } return getPrimaryClipMethod; } @@ -145,8 +153,10 @@ public final class ClipboardManager { case 4: // The last boolean parameter is "userOperate" return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - default: + case 5: return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); + default: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null); } } From 80ca7b15e5f8df6d6940b0dce779fb73d3bad1c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Jul 2024 22:34:58 +0200 Subject: [PATCH 15/45] Extract sources paths in build_without_gradle.sh This avoids duplication, and will be useful to add more packages. --- server/build_without_gradle.sh | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 74bbd8ae..845b0104 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -50,14 +50,24 @@ cd "$SERVER_DIR/src/main/aidl" android/content/IOnPrimaryClipChangedListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl +SRC=( \ + com/genymobile/scrcpy/*.java \ + com/genymobile/scrcpy/wrappers/*.java \ +) + +CLASSES=() +for src in "${SRC[@]}" +do + CLASSES+=("${src%.java}.class") +done + echo "Compiling java sources..." cd ../java javac -bootclasspath "$ANDROID_JAR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \ -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ - com/genymobile/scrcpy/*.java \ - com/genymobile/scrcpy/wrappers/*.java + ${SRC[@]} echo "Dexing..." cd "$CLASSES_DIR" @@ -68,8 +78,7 @@ then "$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \ android/view/*.class \ android/content/*.class \ - com/genymobile/scrcpy/*.class \ - com/genymobile/scrcpy/wrappers/*.class + ${CLASSES[@]} echo "Archiving..." cd "$BUILD_DIR" @@ -81,8 +90,7 @@ else --output "$BUILD_DIR/classes.zip" \ android/view/*.class \ android/content/*.class \ - com/genymobile/scrcpy/*.class \ - com/genymobile/scrcpy/wrappers/*.class + ${CLASSES[@]} cd "$BUILD_DIR" mv classes.zip "$SERVER_BINARY" From e84db2914dfa99ac9be7d789016f08b3e97e089e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Jul 2024 22:38:00 +0200 Subject: [PATCH 16/45] Reorganize server packages There are now a lot of classes in the server, reorganize them into subpackages. --- server/build_without_gradle.sh | 5 +++++ .../java/com/genymobile/scrcpy/CleanUp.java | 5 +++++ .../java/com/genymobile/scrcpy/Options.java | 10 +++++++++ .../java/com/genymobile/scrcpy/Server.java | 21 +++++++++++++++++++ .../com/genymobile/scrcpy/Workarounds.java | 2 ++ .../scrcpy/{ => audio}/AudioCapture.java | 5 ++++- .../AudioCaptureForegroundException.java | 2 +- .../scrcpy/{ => audio}/AudioCodec.java | 4 +++- .../scrcpy/{ => audio}/AudioEncoder.java | 12 ++++++++++- .../scrcpy/{ => audio}/AudioRawRecorder.java | 7 ++++++- .../scrcpy/{ => audio}/AudioSource.java | 4 ++-- .../scrcpy/{ => control}/ControlChannel.java | 2 +- .../scrcpy/{ => control}/ControlMessage.java | 4 +++- .../{ => control}/ControlMessageReader.java | 6 +++++- .../scrcpy/{ => control}/Controller.java | 8 ++++++- .../scrcpy/{ => control}/DeviceMessage.java | 2 +- .../{ => control}/DeviceMessageSender.java | 4 +++- .../{ => control}/DeviceMessageWriter.java | 5 ++++- .../scrcpy/{ => control}/KeyComposition.java | 2 +- .../scrcpy/{ => control}/Pointer.java | 4 +++- .../scrcpy/{ => control}/PointersState.java | 4 +++- .../scrcpy/{ => control}/UhidManager.java | 4 +++- .../{ => device}/ConfigurationException.java | 2 +- .../{ => device}/DesktopConnection.java | 6 +++++- .../scrcpy/{ => device}/Device.java | 6 +++++- .../scrcpy/{ => device}/DisplayInfo.java | 2 +- .../genymobile/scrcpy/{ => device}/Point.java | 2 +- .../scrcpy/{ => device}/Position.java | 2 +- .../genymobile/scrcpy/{ => device}/Size.java | 2 +- .../scrcpy/{ => device}/Streamer.java | 6 +++++- .../genymobile/scrcpy/{ => util}/Binary.java | 2 +- .../genymobile/scrcpy/{ => util}/Codec.java | 2 +- .../scrcpy/{ => util}/CodecOption.java | 2 +- .../scrcpy/{ => util}/CodecUtils.java | 5 ++++- .../genymobile/scrcpy/{ => util}/Command.java | 2 +- .../scrcpy/{ => util}/HandlerExecutor.java | 2 +- .../com/genymobile/scrcpy/{ => util}/IO.java | 4 +++- .../com/genymobile/scrcpy/{ => util}/Ln.java | 4 ++-- .../scrcpy/{ => util}/LogUtils.java | 4 +++- .../scrcpy/{ => util}/Settings.java | 2 +- .../scrcpy/{ => util}/SettingsException.java | 2 +- .../scrcpy/{ => util}/StringUtils.java | 2 +- .../scrcpy/{ => video}/CameraAspectRatio.java | 2 +- .../scrcpy/{ => video}/CameraCapture.java | 5 ++++- .../scrcpy/{ => video}/CameraFacing.java | 4 ++-- .../scrcpy/{ => video}/ScreenCapture.java | 5 ++++- .../scrcpy/{ => video}/ScreenInfo.java | 7 ++++++- .../scrcpy/{ => video}/SurfaceCapture.java | 4 +++- .../scrcpy/{ => video}/SurfaceEncoder.java | 13 +++++++++++- .../scrcpy/{ => video}/VideoCodec.java | 4 +++- .../scrcpy/{ => video}/VideoSource.java | 4 ++-- .../scrcpy/wrappers/ActivityManager.java | 2 +- .../scrcpy/wrappers/ClipboardManager.java | 2 +- .../scrcpy/wrappers/ContentProvider.java | 4 ++-- .../scrcpy/wrappers/DisplayControl.java | 2 +- .../scrcpy/wrappers/DisplayManager.java | 8 +++---- .../scrcpy/wrappers/InputManager.java | 2 +- .../scrcpy/wrappers/PowerManager.java | 2 +- .../scrcpy/wrappers/StatusBarManager.java | 2 +- .../scrcpy/wrappers/SurfaceControl.java | 2 +- .../scrcpy/wrappers/WindowManager.java | 2 +- .../ControlMessageReaderTest.java | 4 +++- .../DeviceMessageWriterTest.java | 2 +- .../scrcpy/{ => util}/BinaryTest.java | 2 +- .../scrcpy/{ => util}/CodecOptionsTest.java | 2 +- .../scrcpy/{ => util}/CommandParserTest.java | 3 ++- .../scrcpy/{ => util}/StringUtilsTest.java | 2 +- 67 files changed, 204 insertions(+), 70 deletions(-) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioCapture.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioCaptureForegroundException.java (84%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioCodec.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioEncoder.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioRawRecorder.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioSource.java (86%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/ControlChannel.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/ControlMessage.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/ControlMessageReader.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/Controller.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/DeviceMessage.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/DeviceMessageSender.java (94%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/DeviceMessageWriter.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/KeyComposition.java (99%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/Pointer.java (92%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/PointersState.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/UhidManager.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/ConfigurationException.java (78%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/DesktopConnection.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Device.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/DisplayInfo.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Point.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Position.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Size.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Streamer.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Binary.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Codec.java (82%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/CodecOption.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/CodecUtils.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Command.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/HandlerExecutor.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/IO.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Ln.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/LogUtils.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Settings.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/SettingsException.java (92%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/StringUtils.java (94%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/CameraAspectRatio.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/CameraCapture.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/CameraFacing.java (89%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/ScreenCapture.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/ScreenInfo.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/SurfaceCapture.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/SurfaceEncoder.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/VideoCodec.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/VideoSource.java (80%) rename server/src/test/java/com/genymobile/scrcpy/{ => control}/ControlMessageReaderTest.java (99%) rename server/src/test/java/com/genymobile/scrcpy/{ => control}/DeviceMessageWriterTest.java (98%) rename server/src/test/java/com/genymobile/scrcpy/{ => util}/BinaryTest.java (98%) rename server/src/test/java/com/genymobile/scrcpy/{ => util}/CodecOptionsTest.java (99%) rename server/src/test/java/com/genymobile/scrcpy/{ => util}/CommandParserTest.java (99%) rename server/src/test/java/com/genymobile/scrcpy/{ => util}/StringUtilsTest.java (97%) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 845b0104..6dc547d9 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -52,6 +52,11 @@ cd "$SERVER_DIR/src/main/aidl" SRC=( \ com/genymobile/scrcpy/*.java \ + com/genymobile/scrcpy/audio/*.java \ + com/genymobile/scrcpy/control/*.java \ + com/genymobile/scrcpy/device/*.java \ + com/genymobile/scrcpy/util/*.java \ + com/genymobile/scrcpy/video/*.java \ com/genymobile/scrcpy/wrappers/*.java \ ) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f9b1efd6..1b8d4248 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -1,5 +1,10 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.Settings; +import com.genymobile.scrcpy.util.SettingsException; + import java.io.File; import java.io.IOException; import java.io.OutputStream; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 9b1d8d8d..143fbb9a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -1,5 +1,15 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.audio.AudioSource; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.CodecOption; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.video.CameraAspectRatio; +import com.genymobile.scrcpy.video.CameraFacing; +import com.genymobile.scrcpy.video.VideoCodec; +import com.genymobile.scrcpy.video.VideoSource; + import android.graphics.Rect; import java.util.List; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 587a46df..263c784d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,5 +1,26 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.audio.AudioCapture; +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.audio.AudioEncoder; +import com.genymobile.scrcpy.audio.AudioRawRecorder; +import com.genymobile.scrcpy.control.ControlChannel; +import com.genymobile.scrcpy.control.Controller; +import com.genymobile.scrcpy.control.DeviceMessage; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.DesktopConnection; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.Streamer; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.util.Settings; +import com.genymobile.scrcpy.util.SettingsException; +import com.genymobile.scrcpy.video.CameraCapture; +import com.genymobile.scrcpy.video.ScreenCapture; +import com.genymobile.scrcpy.video.SurfaceCapture; +import com.genymobile.scrcpy.video.SurfaceEncoder; +import com.genymobile.scrcpy.video.VideoSource; + import android.os.BatteryManager; import android.os.Build; diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index c9a26d78..8b8b4233 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.util.Ln; + import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Application; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/AudioCapture.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 3934ad49..414bfa5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -1,5 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.Workarounds; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java similarity index 84% rename from server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java index baa7d846..49cfe70f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; /** * Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground. diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/AudioCodec.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java index b4ea3680..8f9e59b3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.util.Codec; import android.media.MediaFormat; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 0b59369b..78b6de55 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -1,4 +1,14 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.util.Codec; +import com.genymobile.scrcpy.util.CodecOption; +import com.genymobile.scrcpy.util.CodecUtils; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.util.IO; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.device.Streamer; import android.annotation.TargetApi; import android.media.MediaCodec; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 7e052f32..72527600 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -1,4 +1,9 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.util.IO; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.os.Build; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java similarity index 86% rename from server/src/main/java/com/genymobile/scrcpy/AudioSource.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 466ea297..2324f1a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; import android.media.MediaRecorder; @@ -18,7 +18,7 @@ public enum AudioSource { return value; } - static AudioSource findByName(String name) { + public static AudioSource findByName(String name) { for (AudioSource audioSource : AudioSource.values()) { if (name.equals(audioSource.name)) { return audioSource; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/ControlChannel.java rename to server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java index 4677cfda..f24ca117 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; import android.net.LocalSocket; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/ControlMessage.java rename to server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index bcbacb4b..c414f2a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Position; /** * Union of all supported event types, identified by their {@code type}. diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java rename to server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 1761d228..f5cfee75 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -1,4 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.util.Binary; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Position; import java.io.EOFException; import java.io.IOException; diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/Controller.java rename to server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b7d2f93e..85425113 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -1,5 +1,11 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; +import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.CleanUp; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Point; +import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java index a8987eb6..079a7a04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; public final class DeviceMessage { diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java similarity index 94% rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java index af14bb4e..dc5e6be0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.util.Ln; import java.io.IOException; import java.util.concurrent.ArrayBlockingQueue; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java index f5d57c98..6bf53bed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java @@ -1,4 +1,7 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.StringUtils; import java.io.IOException; import java.io.OutputStream; diff --git a/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java b/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java similarity index 99% rename from server/src/main/java/com/genymobile/scrcpy/KeyComposition.java rename to server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java index 2f2835c9..5b988f53 100644 --- a/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; import java.util.HashMap; import java.util.Map; diff --git a/server/src/main/java/com/genymobile/scrcpy/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java similarity index 92% rename from server/src/main/java/com/genymobile/scrcpy/Pointer.java rename to server/src/main/java/com/genymobile/scrcpy/control/Pointer.java index b89cc256..02e33e10 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Pointer.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Point; public class Pointer { diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/PointersState.java rename to server/src/main/java/com/genymobile/scrcpy/control/PointersState.java index d8daaff2..a12da71d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/PointersState.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Point; import android.view.MotionEvent; diff --git a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/UhidManager.java rename to server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index a39288a5..b1e6a9b9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.util.Ln; import android.os.Build; import android.os.HandlerThread; diff --git a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java b/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java similarity index 78% rename from server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java rename to server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java index 76c8f52e..17729342 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; public class ConfigurationException extends Exception { public ConfigurationException(String message) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java rename to server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java index d693ad61..db75aec6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java @@ -1,4 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; + +import com.genymobile.scrcpy.control.ControlChannel; +import com.genymobile.scrcpy.util.IO; +import com.genymobile.scrcpy.util.StringUtils; import android.net.LocalServerSocket; import android.net.LocalSocket; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/Device.java rename to server/src/main/java/com/genymobile/scrcpy/device/Device.java index 8d0ee231..ae4f50e5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,5 +1,9 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; +import com.genymobile.scrcpy.Options; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java rename to server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index 4b8036f8..2973710d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; public final class DisplayInfo { private final int displayId; diff --git a/server/src/main/java/com/genymobile/scrcpy/Point.java b/server/src/main/java/com/genymobile/scrcpy/device/Point.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/Point.java rename to server/src/main/java/com/genymobile/scrcpy/device/Point.java index c2a30fa8..361b9958 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Point.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Point.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; import java.util.Objects; diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/device/Position.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/Position.java rename to server/src/main/java/com/genymobile/scrcpy/device/Position.java index 2d298645..7ce4e256 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Position.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; import java.util.Objects; diff --git a/server/src/main/java/com/genymobile/scrcpy/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/Size.java rename to server/src/main/java/com/genymobile/scrcpy/device/Size.java index fd4b6971..bc9dce1c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; import android.graphics.Rect; diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/Streamer.java rename to server/src/main/java/com/genymobile/scrcpy/device/Streamer.java index 8b6c9dcc..f54d0567 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java @@ -1,4 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; + +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.util.Codec; +import com.genymobile.scrcpy.util.IO; import android.media.MediaCodec; diff --git a/server/src/main/java/com/genymobile/scrcpy/Binary.java b/server/src/main/java/com/genymobile/scrcpy/util/Binary.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/Binary.java rename to server/src/main/java/com/genymobile/scrcpy/util/Binary.java index 29534f59..f46ba695 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Binary.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Binary.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; public final class Binary { private Binary() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Codec.java b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java similarity index 82% rename from server/src/main/java/com/genymobile/scrcpy/Codec.java rename to server/src/main/java/com/genymobile/scrcpy/util/Codec.java index 7e905af3..a363bd8b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Codec.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; public interface Codec { diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/CodecOption.java rename to server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java index 22c45a90..bed2be9a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import java.util.ArrayList; import java.util.List; diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/CodecUtils.java rename to server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java index afb6f904..5b0c95e8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java @@ -1,4 +1,7 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; + +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.video.VideoCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/util/Command.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/Command.java rename to server/src/main/java/com/genymobile/scrcpy/util/Command.java index 362504ff..b26158e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Command.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Command.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import java.io.IOException; import java.util.Arrays; diff --git a/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java b/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java rename to server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java index 1f5f0a4f..03309989 100644 --- a/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import android.os.Handler; diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/IO.java rename to server/src/main/java/com/genymobile/scrcpy/util/IO.java index 4a55c152..ab3fa59f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; + +import com.genymobile.scrcpy.BuildConfig; import android.system.ErrnoException; import android.system.Os; diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/Ln.java rename to server/src/main/java/com/genymobile/scrcpy/util/Ln.java index cdd57b9f..c0700125 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import android.util.Log; @@ -19,7 +19,7 @@ public final class Ln { private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out)); private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err)); - enum Level { + public enum Level { VERBOSE, DEBUG, INFO, WARN, ERROR } diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/LogUtils.java rename to server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 1ffb19d3..aee1594a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,5 +1,7 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/Settings.java rename to server/src/main/java/com/genymobile/scrcpy/util/Settings.java index 1b5e5f98..d9e82d62 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/SettingsException.java b/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java similarity index 92% rename from server/src/main/java/com/genymobile/scrcpy/SettingsException.java rename to server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java index 36ef63ee..87fa3884 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SettingsException.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; public class SettingsException extends Exception { private static String createMessage(String method, String table, String key, String value) { diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java similarity index 94% rename from server/src/main/java/com/genymobile/scrcpy/StringUtils.java rename to server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java index dac05466..8b19ca3d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; public final class StringUtils { private StringUtils() { diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java rename to server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java index 4fdf4c74..bf1cba5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; public final class CameraAspectRatio { private static final float SENSOR = -1; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/CameraCapture.java rename to server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index df3cf7c4..7d2e2055 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,5 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.util.HandlerExecutor; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java similarity index 89% rename from server/src/main/java/com/genymobile/scrcpy/CameraFacing.java rename to server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java index b7e8daa5..f818e665 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; import android.annotation.SuppressLint; import android.hardware.camera2.CameraCharacteristics; @@ -21,7 +21,7 @@ public enum CameraFacing { return value; } - static CameraFacing findByName(String name) { + public static CameraFacing findByName(String name) { for (CameraFacing facing : CameraFacing.values()) { if (name.equals(facing.name)) { return facing; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java rename to server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 090c96f0..fbeca2af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,5 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java rename to server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index 8e5b401f..ba537b17 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -1,4 +1,9 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.BuildConfig; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import android.graphics.Rect; diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java rename to server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index e300e4d6..3118ddc8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.device.Size; import android.view.Surface; diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java rename to server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 4a0fdf4e..8fe0b227 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -1,4 +1,15 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.util.Codec; +import com.genymobile.scrcpy.util.CodecOption; +import com.genymobile.scrcpy.util.CodecUtils; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.util.IO; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.media.MediaCodecInfo; diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/VideoCodec.java rename to server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java index fa787a99..5d528da1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.util.Codec; import android.annotation.SuppressLint; import android.media.MediaFormat; diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoSource.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java similarity index 80% rename from server/src/main/java/com/genymobile/scrcpy/VideoSource.java rename to server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java index b5a74fbe..53b54a52 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; public enum VideoSource { DISPLAY("display"), @@ -10,7 +10,7 @@ public enum VideoSource { this.name = name; } - static VideoSource findByName(String name) { + public static VideoSource findByName(String name) { for (VideoSource videoSource : VideoSource.values()) { if (name.equals(videoSource.name)) { return videoSource; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index d4bee165..bb1ca0d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -1,7 +1,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index a0e3a7e1..c5f007fe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,7 +1,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.content.ClipData; import android.content.IOnPrimaryClipChangedListener; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index a03f824e..7e92ac50 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.Ln; -import com.genymobile.scrcpy.SettingsException; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.SettingsException; import android.annotation.SuppressLint; import android.content.AttributionSource; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java index ba3e9ee0..cc9d5526 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 2ff82d04..dd92330c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,9 +1,9 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Command; -import com.genymobile.scrcpy.DisplayInfo; -import com.genymobile.scrcpy.Ln; -import com.genymobile.scrcpy.Size; +import com.genymobile.scrcpy.util.Command; +import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import android.annotation.SuppressLint; import android.hardware.display.VirtualDisplay; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 16ecb09f..5c5ba56c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.view.InputEvent; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 36d5f1ac..0a56f347 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.os.Build; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index af217da2..ca80dde2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.os.IInterface; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index f0e351a2..fc18a8e2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.graphics.Rect; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 44394ba9..4c769e85 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.os.IInterface; diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java similarity index 99% rename from server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java rename to server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 0c8086f7..1737730f 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Device; import android.view.KeyEvent; import android.view.MotionEvent; diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java similarity index 98% rename from server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java rename to server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java index d7f926ba..ff1a2fbc 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; import org.junit.Assert; import org.junit.Test; diff --git a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java b/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java similarity index 98% rename from server/src/test/java/com/genymobile/scrcpy/BinaryTest.java rename to server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java index 569a2f2c..7ee95ac5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import org.junit.Assert; import org.junit.Test; diff --git a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java b/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java similarity index 99% rename from server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java rename to server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java index ad802258..ffd8e32e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import org.junit.Assert; import org.junit.Test; diff --git a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java similarity index 99% rename from server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java rename to server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java index de996a07..7e1d55b5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java @@ -1,5 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.wrappers.DisplayManager; import android.view.Display; diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java similarity index 97% rename from server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java rename to server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java index 89799c5e..c72b112a 100644 --- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import org.junit.Assert; import org.junit.Test; From c57a0512ba5b49534c0c10142998e2aa4172ce01 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 19:52:47 +0200 Subject: [PATCH 17/45] Add assertions Passing an unknown enum value to convert them to string would return NULL without any error, possibly causing undefined behavior later. Add assertions to catch such programming errors early. --- app/src/server.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 4d55e994..721c91df 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -147,7 +147,7 @@ log_level_to_server_string(enum sc_log_level level) { return "error"; default: assert(!"unexpected log level"); - return "(unknown)"; + return NULL; } } @@ -183,6 +183,7 @@ sc_server_get_codec_name(enum sc_codec codec) { case SC_CODEC_RAW: return "raw"; default: + assert(!"unexpected codec"); return NULL; } } @@ -197,6 +198,7 @@ sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) { case SC_CAMERA_FACING_EXTERNAL: return "external"; default: + assert(!"unexpected camera facing"); return NULL; } } From bbcd7636121d03db2b7f46c0c2109082c5ae5632 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Jul 2024 18:00:27 +0200 Subject: [PATCH 18/45] Exclude install-release tags from git describe The install_release.sh script is updated one commit after the release tag, which may be confusing. For convenience, new lightweight tags have been added (for example v2.5-install-release) to point to the commit where install_release.sh is updated. But these tags interfere with "git describe" to generate pretty filenames when executing ./release.sh on a development branch, so ignore them. Before: release-v2.5-install-release-17-gc57a0512b After: release-v2.5-18-gc57a0512b Refs #4098 comment --- release.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.mk b/release.mk index 89f3da21..dd544bae 100644 --- a/release.mk +++ b/release.mk @@ -24,7 +24,7 @@ SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 WIN64_BUILD_DIR := build-win64 -VERSION := $(shell git describe --tags --always) +VERSION := $(shell git describe --tags --exclude='*install-release' --always) DIST := dist WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) From e0cdc2ace32c63892d8fa6f1c22cbbdae5622bea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Jul 2024 18:02:26 +0200 Subject: [PATCH 19/45] Fix method name The method indicates whether GetPhysicalDisplayIds() exists. The "Get" was missing. --- server/src/main/java/com/genymobile/scrcpy/device/Device.java | 2 +- .../java/com/genymobile/scrcpy/wrappers/SurfaceControl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index ae4f50e5..46657a05 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -326,7 +326,7 @@ public final class Device { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // On Android 14, these internal methods have been moved to DisplayControl boolean useDisplayControl = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod(); + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); // Change the power mode for all physical displays long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index fc18a8e2..2f24f2d2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -134,7 +134,7 @@ public final class SurfaceControl { return getPhysicalDisplayIdsMethod; } - public static boolean hasPhysicalDisplayIdsMethod() { + public static boolean hasGetPhysicalDisplayIdsMethod() { try { getGetPhysicalDisplayIdsMethod(); return true; From 9d1d79b004ed7171f481225993d54c3b100411a7 Mon Sep 17 00:00:00 2001 From: Kaiming Hu Date: Wed, 17 Jul 2024 20:20:13 +0800 Subject: [PATCH 20/45] Fix "turn screen off" for Honor Android 14 devices Fixes #4823 PR #5109 Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/device/Device.java | 14 +++++++++++++- .../genymobile/scrcpy/wrappers/SurfaceControl.java | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 46657a05..5a1083fd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -323,7 +323,19 @@ public final class Device { * @param mode one of the {@code POWER_MODE_*} constants */ public static boolean setScreenPowerMode(int mode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + + if (applyToMultiPhysicalDisplays + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + && Build.BRAND.equalsIgnoreCase("honor") + && SurfaceControl.hasGetBuildInDisplayMethod()) { + // Workaround for Honor devices with Android 14: + // - + // - + applyToMultiPhysicalDisplays = false; + } + + if (applyToMultiPhysicalDisplays) { // On Android 14, these internal methods have been moved to DisplayControl boolean useDisplayControl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 2f24f2d2..038e7ca0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -94,6 +94,15 @@ public final class SurfaceControl { return getBuiltInDisplayMethod; } + public static boolean hasGetBuildInDisplayMethod() { + try { + getGetBuiltInDisplayMethod(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + public static IBinder getBuiltInDisplay() { try { Method method = getGetBuiltInDisplayMethod(); From 39132ff2dd7f85af7aa03d68df3d42e5b6646b97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 19:55:53 +0200 Subject: [PATCH 21/45] Make encode() method private It is only used from AudioEncoder. PR #5102 --- .../src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 78b6de55..e45284af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -176,7 +176,7 @@ public final class AudioEncoder implements AsyncProcessor { } @TargetApi(Build.VERSION_CODES.M) - public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { + private void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); From 3b8ec0c38db43a5dd5ed46bc416743ba8e169408 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:08:45 +0200 Subject: [PATCH 22/45] Rename audio capture exception The AudioCaptureForegroundException was very specific. Rename it to AudioCaptureException to support other capture failures. PR #5102 --- .../com/genymobile/scrcpy/audio/AudioCapture.java | 6 +++--- .../scrcpy/audio/AudioCaptureException.java | 12 ++++++++++++ .../audio/AudioCaptureForegroundException.java | 7 ------- .../com/genymobile/scrcpy/audio/AudioEncoder.java | 4 ++-- .../genymobile/scrcpy/audio/AudioRawRecorder.java | 4 ++-- 5 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java delete mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 414bfa5d..1da2221e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -89,7 +89,7 @@ public final class AudioCapture { ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); } - private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException { + private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException { while (attempts-- > 0) { // Wait for activity to start SystemClock.sleep(delayMs); @@ -101,7 +101,7 @@ public final class AudioCapture { Ln.e("Failed to start audio capture"); Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + "scrcpy."); - throw new AudioCaptureForegroundException(); + throw new AudioCaptureException(); } else { Ln.d("Failed to start audio capture, retrying..."); } @@ -121,7 +121,7 @@ public final class AudioCapture { recorder.startRecording(); } - public void start() throws AudioCaptureForegroundException { + public void start() throws AudioCaptureException { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { startWorkaroundAndroid11(); try { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java new file mode 100644 index 00000000..4b0b7e83 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java @@ -0,0 +1,12 @@ +package com.genymobile.scrcpy.audio; + +/** + * Exception for any audio capture issue. + *

+ * This includes the case where audio capture failed on Android 11 specifically because the running App (Shell) was not in foreground. + *

+ * Its purpose is to disable audio without errors (that's why the exception is empty, any error message must be printed by the caller before + * throwing the exception). + */ +public class AudioCaptureException extends Exception { +} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java deleted file mode 100644 index 49cfe70f..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.genymobile.scrcpy.audio; - -/** - * Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground. - */ -public class AudioCaptureForegroundException extends Exception { -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index e45284af..219e2c0c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -132,7 +132,7 @@ public final class AudioEncoder implements AsyncProcessor { } catch (ConfigurationException e) { // Do not print stack trace, a user-friendly error-message has already been logged fatalError = true; - } catch (AudioCaptureForegroundException e) { + } catch (AudioCaptureException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { Ln.e("Audio encoding error", e); @@ -176,7 +176,7 @@ public final class AudioEncoder implements AsyncProcessor { } @TargetApi(Build.VERSION_CODES.M) - private void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { + private void encode() throws IOException, ConfigurationException, AudioCaptureException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 72527600..c7279a3a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -23,7 +23,7 @@ public final class AudioRawRecorder implements AsyncProcessor { this.streamer = streamer; } - private void record() throws IOException, AudioCaptureForegroundException { + private void record() throws IOException, AudioCaptureException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); @@ -69,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor { boolean fatalError = false; try { record(); - } catch (AudioCaptureForegroundException e) { + } catch (AudioCaptureException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (Throwable t) { Ln.e("Audio recording error", t); From cf09e78323775f97516e09c17c1201b4f39be940 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 20:00:46 +0200 Subject: [PATCH 23/45] Throw AudioCaptureException on workaround error Replace a RuntimeException by a specific AudioCaptureException. PR #5102 --- .../src/main/java/com/genymobile/scrcpy/Workarounds.java | 8 +++++--- .../java/com/genymobile/scrcpy/audio/AudioCapture.java | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 8b8b4233..6a4a57e1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.audio.AudioCaptureException; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -195,7 +196,8 @@ public final class Workarounds { @TargetApi(Build.VERSION_CODES.R) @SuppressLint("WrongConstant,MissingPermission") - public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { + public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws + AudioCaptureException { // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. // // This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses @@ -336,8 +338,8 @@ public final class Workarounds { return audioRecord; } catch (Exception e) { - Ln.e("Failed to invoke AudioRecord..", e); - throw new RuntimeException("Cannot create AudioRecord"); + Ln.e("Cannot create AudioRecord", e); + throw new AudioCaptureException(); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 1da2221e..609ea4c2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -109,7 +109,7 @@ public final class AudioCapture { } } - private void startRecording() { + private void startRecording() throws AudioCaptureException { try { recorder = createAudioRecord(audioSource); } catch (NullPointerException e) { From 5e605b9b8f8a31b53a41695a831696edad29327e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 13:02:07 +0200 Subject: [PATCH 24/45] Move audio compatibility check The compatibility depends on the capture constraints, not the encoding. This will allow to add a new capture implementation with different constraints. PR #5102 --- .../java/com/genymobile/scrcpy/audio/AudioCapture.java | 7 +++++++ .../java/com/genymobile/scrcpy/audio/AudioEncoder.java | 2 ++ 2 files changed, 9 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 609ea4c2..27ea1ec1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -121,6 +121,13 @@ public final class AudioCapture { recorder.startRecording(); } + public void checkCompatibility() throws AudioCaptureException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Ln.w("Audio disabled: it is not supported before Android 11"); + throw new AudioCaptureException(); + } + } + public void start() throws AudioCaptureException { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { startWorkaroundAndroid11(); diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 219e2c0c..3eadf51a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -187,6 +187,8 @@ public final class AudioEncoder implements AsyncProcessor { boolean mediaCodecStarted = false; try { + capture.checkCompatibility(); // throws an AudioCaptureException on error + Codec codec = streamer.getCodec(); mediaCodec = createMediaCodec(codec, encoderName); From a2f3a5cf1887261ff526245a9e9e9caa3fb87385 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:10:07 +0200 Subject: [PATCH 25/45] Move hardcoded audio configuration to AudioConfig This will allow to use these constants from different classes not directly related to AudioCapture. PR #5102 --- .../genymobile/scrcpy/audio/AudioCapture.java | 17 +++++++-------- .../genymobile/scrcpy/audio/AudioConfig.java | 21 +++++++++++++++++++ .../genymobile/scrcpy/audio/AudioEncoder.java | 4 ++-- .../scrcpy/audio/AudioRawRecorder.java | 2 +- 4 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 27ea1ec1..4b7fb8c6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -20,17 +20,14 @@ import java.nio.ByteBuffer; public final class AudioCapture { - public static final int SAMPLE_RATE = 48000; - public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; - public static final int CHANNELS = 2; - public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT; - public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; - public static final int BYTES_PER_SAMPLE = 2; + private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; + private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG; + private static final int CHANNELS = AudioConfig.CHANNELS; + private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; + private static final int ENCODING = AudioConfig.ENCODING; + private static final int BYTES_PER_SAMPLE = AudioConfig.BYTES_PER_SAMPLE; - // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency). - // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we - // receive 4 successive blocks without waiting, then we wait for the 4 next ones). - public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + private static final int MAX_READ_SIZE = AudioConfig.MAX_READ_SIZE; private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java new file mode 100644 index 00000000..b4d79774 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy.audio; + +import android.media.AudioFormat; + +public final class AudioConfig { + public static final int SAMPLE_RATE = 48000; + public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; + public static final int CHANNELS = 2; + public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT; + public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; + public static final int BYTES_PER_SAMPLE = 2; + + // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency). + // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we + // receive 4 successive blocks without waiting, then we wait for the 4 next ones). + public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + + private AudioConfig() { + // Not instantiable + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 3eadf51a..8230e054 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -44,8 +44,8 @@ public final class AudioEncoder implements AsyncProcessor { } } - private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE; - private static final int CHANNELS = AudioCapture.CHANNELS; + private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; + private static final int CHANNELS = AudioConfig.CHANNELS; private final AudioCapture capture; private final Streamer streamer; diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index c7279a3a..323caae4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -30,7 +30,7 @@ public final class AudioRawRecorder implements AsyncProcessor { return; } - final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE); + final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioConfig.MAX_READ_SIZE); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); try { From 414ce4c7545d62c0f1e34b2978ace5745d199fd4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:14:48 +0200 Subject: [PATCH 26/45] Move createAudioFormat() to AudioConfig This will allow to reuse this method. PR #5102 --- .../com/genymobile/scrcpy/audio/AudioCapture.java | 11 +---------- .../java/com/genymobile/scrcpy/audio/AudioConfig.java | 8 ++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 4b7fb8c6..c1b19dac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -9,7 +9,6 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Intent; -import android.media.AudioFormat; import android.media.AudioRecord; import android.media.AudioTimestamp; import android.media.MediaCodec; @@ -44,14 +43,6 @@ public final class AudioCapture { this.audioSource = audioSource.value(); } - private static AudioFormat createAudioFormat() { - AudioFormat.Builder builder = new AudioFormat.Builder(); - builder.setEncoding(ENCODING); - builder.setSampleRate(SAMPLE_RATE); - builder.setChannelMask(CHANNEL_CONFIG); - return builder.build(); - } - @TargetApi(Build.VERSION_CODES.M) @SuppressLint({"WrongConstant", "MissingPermission"}) private static AudioRecord createAudioRecord(int audioSource) { @@ -61,7 +52,7 @@ public final class AudioCapture { builder.setContext(FakeContext.get()); } builder.setAudioSource(audioSource); - builder.setAudioFormat(createAudioFormat()); + builder.setAudioFormat(AudioConfig.createAudioFormat()); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); // This buffer size does not impact latency builder.setBufferSizeInBytes(8 * minBufferSize); diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java index b4d79774..c77165a7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java @@ -18,4 +18,12 @@ public final class AudioConfig { private AudioConfig() { // Not instantiable } + + public static AudioFormat createAudioFormat() { + AudioFormat.Builder builder = new AudioFormat.Builder(); + builder.setEncoding(ENCODING); + builder.setSampleRate(SAMPLE_RATE); + builder.setChannelMask(CHANNEL_CONFIG); + return builder.build(); + } } From 053bf83f581be543815cf859cedcb496fbb0bc75 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:28:13 +0200 Subject: [PATCH 27/45] Extract AudioRecordReader Move the logic to read from an AudioRecord and handle all corner cases for PTS. This simplifies AudioCapture. PR #5102 --- .../genymobile/scrcpy/audio/AudioCapture.java | 51 ++------------ .../scrcpy/audio/AudioRecordReader.java | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 47 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index c1b19dac..cfc3455e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -10,7 +10,6 @@ import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Intent; import android.media.AudioRecord; -import android.media.AudioTimestamp; import android.media.MediaCodec; import android.os.Build; import android.os.SystemClock; @@ -24,20 +23,11 @@ public final class AudioCapture { private static final int CHANNELS = AudioConfig.CHANNELS; private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; private static final int ENCODING = AudioConfig.ENCODING; - private static final int BYTES_PER_SAMPLE = AudioConfig.BYTES_PER_SAMPLE; - - private static final int MAX_READ_SIZE = AudioConfig.MAX_READ_SIZE; - - private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) private final int audioSource; private AudioRecord recorder; - - private final AudioTimestamp timestamp = new AudioTimestamp(); - private long previousRecorderTimestamp = -1; - private long previousPts = 0; - private long nextPts = 0; + private AudioRecordReader reader; public AudioCapture(AudioSource audioSource) { this.audioSource = audioSource.value(); @@ -107,6 +97,7 @@ public final class AudioCapture { recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); } recorder.startRecording(); + reader = new AudioRecordReader(recorder); } public void checkCompatibility() throws AudioCaptureException { @@ -137,41 +128,7 @@ public final class AudioCapture { } @TargetApi(Build.VERSION_CODES.N) - public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) { - int r = recorder.read(directBuffer, MAX_READ_SIZE); - if (r <= 0) { - return r; - } - - long pts; - - int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); - if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { - pts = timestamp.nanoTime / 1000; - previousRecorderTimestamp = timestamp.nanoTime; - } else { - if (nextPts == 0) { - Ln.w("Could not get initial audio timestamp"); - nextPts = System.nanoTime() / 1000; - } - // compute from previous timestamp and packet size - pts = nextPts; - } - - long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); - nextPts = pts + durationUs; - - if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { - // Audio PTS may come from two sources: - // - recorder.getTimestamp() if the call works; - // - an estimation from the previous PTS and the packet size as a fallback. - // - // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. - pts = previousPts + ONE_SAMPLE_US; - } - previousPts = pts; - - outBufferInfo.set(0, r, pts, 0); - return r; + public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { + return reader.read(outDirectBuffer, outBufferInfo); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java new file mode 100644 index 00000000..80286831 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java @@ -0,0 +1,67 @@ +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.util.Ln; + +import android.annotation.TargetApi; +import android.media.AudioRecord; +import android.media.AudioTimestamp; +import android.media.MediaCodec; +import android.os.Build; + +import java.nio.ByteBuffer; + +public class AudioRecordReader { + + private static final long ONE_SAMPLE_US = + (1000000 + AudioConfig.SAMPLE_RATE - 1) / AudioConfig.SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) + + private final AudioRecord recorder; + + private final AudioTimestamp timestamp = new AudioTimestamp(); + private long previousRecorderTimestamp = -1; + private long previousPts = 0; + private long nextPts = 0; + + public AudioRecordReader(AudioRecord recorder) { + this.recorder = recorder; + } + + @TargetApi(Build.VERSION_CODES.N) + public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { + int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE); + if (r <= 0) { + return r; + } + + long pts; + + int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); + if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { + pts = timestamp.nanoTime / 1000; + previousRecorderTimestamp = timestamp.nanoTime; + } else { + if (nextPts == 0) { + Ln.w("Could not get initial audio timestamp"); + nextPts = System.nanoTime() / 1000; + } + // compute from previous timestamp and packet size + pts = nextPts; + } + + long durationUs = r * 1000000L / (AudioConfig.CHANNELS * AudioConfig.BYTES_PER_SAMPLE * AudioConfig.SAMPLE_RATE); + nextPts = pts + durationUs; + + if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { + // Audio PTS may come from two sources: + // - recorder.getTimestamp() if the call works; + // - an estimation from the previous PTS and the packet size as a fallback. + // + // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. + pts = previousPts + ONE_SAMPLE_US; + } + previousPts = pts; + + outBufferInfo.set(0, r, pts, 0); + return r; + } +} From 0f076083e88cdd313ab98c028598999f38b5a02f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:42:22 +0200 Subject: [PATCH 28/45] Extract AudioCapture interface Move the implementation to AudioDirectCapture and extract an AudioCapture interface. This will allow to provide another AudioCapture implementation. PR #5102 --- .../java/com/genymobile/scrcpy/Server.java | 3 +- .../genymobile/scrcpy/audio/AudioCapture.java | 138 ++---------------- .../scrcpy/audio/AudioDirectCapture.java | 138 ++++++++++++++++++ 3 files changed, 152 insertions(+), 127 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 263c784d..8a88e276 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.audio.AudioCapture; import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.audio.AudioDirectCapture; import com.genymobile.scrcpy.audio.AudioEncoder; import com.genymobile.scrcpy.audio.AudioRawRecorder; import com.genymobile.scrcpy.control.ControlChannel; @@ -163,7 +164,7 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); - AudioCapture audioCapture = new AudioCapture(options.getAudioSource()); + AudioCapture audioCapture = new AudioDirectCapture(options.getAudioSource()); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index cfc3455e..62903f83 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -1,134 +1,20 @@ package com.genymobile.scrcpy.audio; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.Workarounds; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Intent; -import android.media.AudioRecord; import android.media.MediaCodec; -import android.os.Build; -import android.os.SystemClock; import java.nio.ByteBuffer; -public final class AudioCapture { +public interface AudioCapture { + void checkCompatibility() throws AudioCaptureException; + void start() throws AudioCaptureException; + void stop(); - private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; - private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG; - private static final int CHANNELS = AudioConfig.CHANNELS; - private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; - private static final int ENCODING = AudioConfig.ENCODING; - - private final int audioSource; - - private AudioRecord recorder; - private AudioRecordReader reader; - - public AudioCapture(AudioSource audioSource) { - this.audioSource = audioSource.value(); - } - - @TargetApi(Build.VERSION_CODES.M) - @SuppressLint({"WrongConstant", "MissingPermission"}) - private static AudioRecord createAudioRecord(int audioSource) { - AudioRecord.Builder builder = new AudioRecord.Builder(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // On older APIs, Workarounds.fillAppInfo() must be called beforehand - builder.setContext(FakeContext.get()); - } - builder.setAudioSource(audioSource); - builder.setAudioFormat(AudioConfig.createAudioFormat()); - int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); - // This buffer size does not impact latency - builder.setBufferSizeInBytes(8 * minBufferSize); - return builder.build(); - } - - private static void startWorkaroundAndroid11() { - // Android 11 requires Apps to be at foreground to record audio. - // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. - // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android - // shell ("com.android.shell"). - // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the - // foreground. - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); - ServiceManager.getActivityManager().startActivity(intent); - } - - private static void stopWorkaroundAndroid11() { - ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); - } - - private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException { - while (attempts-- > 0) { - // Wait for activity to start - SystemClock.sleep(delayMs); - try { - startRecording(); - return; // it worked - } catch (UnsupportedOperationException e) { - if (attempts == 0) { - Ln.e("Failed to start audio capture"); - Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " - + "scrcpy."); - throw new AudioCaptureException(); - } else { - Ln.d("Failed to start audio capture, retrying..."); - } - } - } - } - - private void startRecording() throws AudioCaptureException { - try { - recorder = createAudioRecord(audioSource); - } catch (NullPointerException e) { - // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: - // - - // - - recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); - } - recorder.startRecording(); - reader = new AudioRecordReader(recorder); - } - - public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - Ln.w("Audio disabled: it is not supported before Android 11"); - throw new AudioCaptureException(); - } - } - - public void start() throws AudioCaptureException { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - startWorkaroundAndroid11(); - try { - tryStartRecording(5, 100); - } finally { - stopWorkaroundAndroid11(); - } - } else { - startRecording(); - } - } - - public void stop() { - if (recorder != null) { - // Will call .stop() if necessary, without throwing an IllegalStateException - recorder.release(); - } - } - - @TargetApi(Build.VERSION_CODES.N) - public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { - return reader.read(outDirectBuffer, outBufferInfo); - } + /** + * Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples. + * + * @param outDirectBuffer The target buffer + * @param outBufferInfo The info to provide to MediaCodec + * @return the number of bytes actually read. + */ + int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java new file mode 100644 index 00000000..c0331467 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -0,0 +1,138 @@ +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.Workarounds; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Intent; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.os.Build; +import android.os.SystemClock; + +import java.nio.ByteBuffer; + +public class AudioDirectCapture implements AudioCapture { + + private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; + private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG; + private static final int CHANNELS = AudioConfig.CHANNELS; + private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; + private static final int ENCODING = AudioConfig.ENCODING; + + private final int audioSource; + + private AudioRecord recorder; + private AudioRecordReader reader; + + public AudioDirectCapture(AudioSource audioSource) { + this.audioSource = audioSource.value(); + } + + @TargetApi(Build.VERSION_CODES.M) + @SuppressLint({"WrongConstant", "MissingPermission"}) + private static AudioRecord createAudioRecord(int audioSource) { + AudioRecord.Builder builder = new AudioRecord.Builder(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On older APIs, Workarounds.fillAppInfo() must be called beforehand + builder.setContext(FakeContext.get()); + } + builder.setAudioSource(audioSource); + builder.setAudioFormat(AudioConfig.createAudioFormat()); + int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); + // This buffer size does not impact latency + builder.setBufferSizeInBytes(8 * minBufferSize); + return builder.build(); + } + + private static void startWorkaroundAndroid11() { + // Android 11 requires Apps to be at foreground to record audio. + // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. + // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android + // shell ("com.android.shell"). + // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the + // foreground. + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); + ServiceManager.getActivityManager().startActivity(intent); + } + + private static void stopWorkaroundAndroid11() { + ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); + } + + private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException { + while (attempts-- > 0) { + // Wait for activity to start + SystemClock.sleep(delayMs); + try { + startRecording(); + return; // it worked + } catch (UnsupportedOperationException e) { + if (attempts == 0) { + Ln.e("Failed to start audio capture"); + Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + + "scrcpy."); + throw new AudioCaptureException(); + } else { + Ln.d("Failed to start audio capture, retrying..."); + } + } + } + } + + private void startRecording() throws AudioCaptureException { + try { + recorder = createAudioRecord(audioSource); + } catch (NullPointerException e) { + // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: + // - + // - + recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); + } + recorder.startRecording(); + reader = new AudioRecordReader(recorder); + } + + @Override + public void checkCompatibility() throws AudioCaptureException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Ln.w("Audio disabled: it is not supported before Android 11"); + throw new AudioCaptureException(); + } + } + + @Override + public void start() throws AudioCaptureException { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + startWorkaroundAndroid11(); + try { + tryStartRecording(5, 100); + } finally { + stopWorkaroundAndroid11(); + } + } else { + startRecording(); + } + } + + @Override + public void stop() { + if (recorder != null) { + // Will call .stop() if necessary, without throwing an IllegalStateException + recorder.release(); + } + } + + @Override + @TargetApi(Build.VERSION_CODES.N) + public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { + return reader.read(outDirectBuffer, outBufferInfo); + } +} From 53c6eb66ea8fc07433ffe686e9994c8d7c104b7a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Jul 2024 10:54:43 +0200 Subject: [PATCH 29/45] Move audio source value The MediaRecorder constant should not belong to the AudioSource enum. This will allow to add a new AudioSource which has no meaningful MediaRecorder audio source value. PR #5102 --- .../scrcpy/audio/AudioDirectCapture.java | 14 +++++++++++++- .../com/genymobile/scrcpy/audio/AudioSource.java | 14 +++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index c0331467..361c7bac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -11,6 +11,7 @@ import android.content.ComponentName; import android.content.Intent; import android.media.AudioRecord; import android.media.MediaCodec; +import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; @@ -30,7 +31,18 @@ public class AudioDirectCapture implements AudioCapture { private AudioRecordReader reader; public AudioDirectCapture(AudioSource audioSource) { - this.audioSource = audioSource.value(); + this.audioSource = getAudioSourceValue(audioSource); + } + + private static int getAudioSourceValue(AudioSource audioSource) { + switch (audioSource) { + case OUTPUT: + return MediaRecorder.AudioSource.REMOTE_SUBMIX; + case MIC: + return MediaRecorder.AudioSource.MIC; + default: + throw new IllegalArgumentException("Unsupported audio source: " + audioSource); + } } @TargetApi(Build.VERSION_CODES.M) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 2324f1a4..7201dd39 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,21 +1,13 @@ package com.genymobile.scrcpy.audio; -import android.media.MediaRecorder; - public enum AudioSource { - OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), - MIC("mic", MediaRecorder.AudioSource.MIC); + OUTPUT("output"), + MIC("mic"); private final String name; - private final int value; - AudioSource(String name, int value) { + AudioSource(String name) { this.name = name; - this.value = value; - } - - int value() { - return value; } public static AudioSource findByName(String name) { From a10f8cd798023f858796b023cb846fa2184ad2c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Jul 2024 10:57:46 +0200 Subject: [PATCH 30/45] Add audio playback capture method Add a new method to capture audio playback. It requires Android 13 (where the Shell app has MODIFY_AUDIO_ROUTING permission). The main benefit is that it supports keeping audio playing on the device (implemented in a further commit). Fixes #4380 PR #5102 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 8 +- app/src/cli.c | 16 ++- app/src/options.h | 1 + app/src/server.c | 20 ++- .../java/com/genymobile/scrcpy/Server.java | 5 +- .../scrcpy/audio/AudioPlaybackCapture.java | 130 ++++++++++++++++++ .../genymobile/scrcpy/audio/AudioSource.java | 7 +- 9 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index b35ea5e4..d5f129d0 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -111,7 +111,7 @@ _scrcpy() { return ;; --audio-source) - COMPREPLY=($(compgen -W 'output mic' -- "$cur")) + COMPREPLY=($(compgen -W 'output mic playback' -- "$cur")) return ;; --camera-facing) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 5afca977..c49c24eb 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -14,7 +14,7 @@ arguments=( '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' - '--audio-source=[Select the audio source]:source:(output mic)' + '--audio-source=[Select the audio source]:source:(output mic playback)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--camera-ar=[Select the camera size by its aspect ratio]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1c0c0f7a..19b4ab6b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -57,7 +57,13 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR. .TP .BI "\-\-audio\-source " source -Select the audio source (output or mic). +Select the audio source (output, mic or playback). + +The "output" source forwards the whole audio output, and disables playback on the device. + +The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + +The "mic" source captures the microphone. Default is output. diff --git a/app/src/cli.c b/app/src/cli.c index 9dd49538..1699e46d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -189,7 +189,13 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_SOURCE, .longopt = "audio-source", .argdesc = "source", - .text = "Select the audio source (output or mic).\n" + .text = "Select the audio source (output, mic or playback).\n" + "The \"output\" source forwards the whole audio output, and " + "disables playback on the device.\n" + "The \"playback\" source captures the audio playback (Android " + "apps can opt-out, so the whole output is not necessarily " + "captured).\n" + "The \"mic\" source captures the microphone.\n" "Default is output.", }, { @@ -1931,7 +1937,13 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { return true; } - LOGE("Unsupported audio source: %s (expected output or mic)", optarg); + if (!strcmp(optarg, "playback")) { + *source = SC_AUDIO_SOURCE_PLAYBACK; + return true; + } + + LOGE("Unsupported audio source: %s (expected output, mic or playback)", + optarg); return false; } diff --git a/app/src/options.h b/app/src/options.h index 5ec809f0..403685e3 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -59,6 +59,7 @@ enum sc_audio_source { SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA SC_AUDIO_SOURCE_OUTPUT, SC_AUDIO_SOURCE_MIC, + SC_AUDIO_SOURCE_PLAYBACK, }; enum sc_camera_facing { diff --git a/app/src/server.c b/app/src/server.c index 721c91df..e32aa556 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -203,6 +203,21 @@ sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) { } } +static const char * +sc_server_get_audio_source_name(enum sc_audio_source audio_source) { + switch (audio_source) { + case SC_AUDIO_SOURCE_OUTPUT: + return "output"; + case SC_AUDIO_SOURCE_MIC: + return "mic"; + case SC_AUDIO_SOURCE_PLAYBACK: + return "playback"; + default: + assert(!"unexpected audio source"); + return NULL; + } +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -273,8 +288,9 @@ execute_server(struct sc_server *server, assert(params->video_source == SC_VIDEO_SOURCE_CAMERA); ADD_PARAM("video_source=camera"); } - if (params->audio_source == SC_AUDIO_SOURCE_MIC) { - ADD_PARAM("audio_source=mic"); + if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) { + ADD_PARAM("audio_source=%s", + sc_server_get_audio_source_name(params->audio_source)); } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 8a88e276..75d9ea15 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -4,7 +4,9 @@ import com.genymobile.scrcpy.audio.AudioCapture; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioDirectCapture; import com.genymobile.scrcpy.audio.AudioEncoder; +import com.genymobile.scrcpy.audio.AudioPlaybackCapture; import com.genymobile.scrcpy.audio.AudioRawRecorder; +import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.control.ControlChannel; import com.genymobile.scrcpy.control.Controller; import com.genymobile.scrcpy.control.DeviceMessage; @@ -164,7 +166,8 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); - AudioCapture audioCapture = new AudioDirectCapture(options.getAudioSource()); + AudioSource audioSource = options.getAudioSource(); + AudioCapture audioCapture = audioSource.isDirect() ? new AudioDirectCapture(audioSource) : new AudioPlaybackCapture(); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java new file mode 100644 index 00000000..2a0d23fb --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -0,0 +1,130 @@ +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.util.Ln; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.os.Build; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +public final class AudioPlaybackCapture implements AudioCapture { + + private AudioRecord recorder; + private AudioRecordReader reader; + + @SuppressLint("PrivateApi") + private AudioRecord createAudioRecord() throws AudioCaptureException { + // See + try { + Class audioMixingRuleClass = Class.forName("android.media.audiopolicy.AudioMixingRule"); + Class audioMixingRuleBuilderClass = Class.forName("android.media.audiopolicy.AudioMixingRule$Builder"); + + // AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder(); + Object audioMixingRuleBuilder = audioMixingRuleBuilderClass.getConstructor().newInstance(); + + // audioMixingRuleBuilder.setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS); + int mixRolePlayersConstant = audioMixingRuleClass.getField("MIX_ROLE_PLAYERS").getInt(null); + Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class); + setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant); + + AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); + + // audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes); + int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null); + Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class); + addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes); + + // AudioMixingRule audioMixingRule = builder.build(); + Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder); + + // audioMixingRuleBuilder.voiceCommunicationCaptureAllowed(true); + Method voiceCommunicationCaptureAllowedMethod = audioMixingRuleBuilderClass.getMethod("voiceCommunicationCaptureAllowed", boolean.class); + voiceCommunicationCaptureAllowedMethod.invoke(audioMixingRuleBuilder, true); + + Class audioMixClass = Class.forName("android.media.audiopolicy.AudioMix"); + Class audioMixBuilderClass = Class.forName("android.media.audiopolicy.AudioMix$Builder"); + + // AudioMix.Builder audioMixBuilder = new AudioMix.Builder(audioMixingRule); + Object audioMixBuilder = audioMixBuilderClass.getConstructor(audioMixingRuleClass).newInstance(audioMixingRule); + + // audioMixBuilder.setFormat(createAudioFormat()); + Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class); + setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat()); + + int routeFlags = audioMixClass.getField("ROUTE_FLAG_LOOP_BACK").getInt(null); + + // audioMixBuilder.setRouteFlags(routeFlag); + Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class); + setRouteFlags.invoke(audioMixBuilder, routeFlags); + + // AudioMix audioMix = audioMixBuilder.build(); + Object audioMix = audioMixBuilderClass.getMethod("build").invoke(audioMixBuilder); + + Class audioPolicyClass = Class.forName("android.media.audiopolicy.AudioPolicy"); + Class audioPolicyBuilderClass = Class.forName("android.media.audiopolicy.AudioPolicy$Builder"); + + // AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder(); + Object audioPolicyBuilder = audioPolicyBuilderClass.getConstructor(Context.class).newInstance(FakeContext.get()); + + // audioPolicyBuilder.addMix(audioMix); + Method addMixMethod = audioPolicyBuilderClass.getMethod("addMix", audioMixClass); + addMixMethod.invoke(audioPolicyBuilder, audioMix); + + // AudioPolicy audioPolicy = audioPolicyBuilder.build(); + Object audioPolicy = audioPolicyBuilderClass.getMethod("build").invoke(audioPolicyBuilder); + + // AudioManager.registerAudioPolicyStatic(audioPolicy); + Method registerAudioPolicyStaticMethod = AudioManager.class.getDeclaredMethod("registerAudioPolicyStatic", audioPolicyClass); + registerAudioPolicyStaticMethod.setAccessible(true); + int result = (int) registerAudioPolicyStaticMethod.invoke(null, audioPolicy); + if (result != 0) { + throw new RuntimeException("registerAudioPolicy() returned " + result); + } + + // audioPolicy.createAudioRecordSink(audioPolicy); + Method createAudioRecordSinkClass = audioPolicyClass.getMethod("createAudioRecordSink", audioMixClass); + return (AudioRecord) createAudioRecordSinkClass.invoke(audioPolicy, audioMix); + } catch (Exception e) { + Ln.e("Could not capture audio playback", e); + throw new AudioCaptureException(); + } + } + + @Override + public void checkCompatibility() throws AudioCaptureException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Ln.w("Audio disabled: audio playback capture source not supported before Android 13"); + throw new AudioCaptureException(); + } + } + + @Override + public void start() throws AudioCaptureException { + recorder = createAudioRecord(); + recorder.startRecording(); + reader = new AudioRecordReader(recorder); + } + + @Override + public void stop() { + if (recorder != null) { + // Will call .stop() if necessary, without throwing an IllegalStateException + recorder.release(); + } + } + + @Override + @TargetApi(Build.VERSION_CODES.N) + public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { + return reader.read(outDirectBuffer, outBufferInfo); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 7201dd39..6082f20e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -2,7 +2,8 @@ package com.genymobile.scrcpy.audio; public enum AudioSource { OUTPUT("output"), - MIC("mic"); + MIC("mic"), + PLAYBACK("playback"); private final String name; @@ -10,6 +11,10 @@ public enum AudioSource { this.name = name; } + public boolean isDirect() { + return this != PLAYBACK; + } + public static AudioSource findByName(String name) { for (AudioSource audioSource : AudioSource.values()) { if (name.equals(audioSource.name)) { From 31116a60d7b03f8489a4f73d81ab0c0689a67d9b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 20:56:18 +0200 Subject: [PATCH 31/45] Add --audio-dup Add an option to duplicate audio on the device, compatible with the new audio playback capture (--audio-source=playback). Fixes #3875 Fixes #4380 PR #5102 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 +++++ app/src/cli.c | 23 +++++++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 +++ app/src/server.h | 1 + .../java/com/genymobile/scrcpy/Options.java | 8 +++++++ .../java/com/genymobile/scrcpy/Server.java | 8 ++++++- .../scrcpy/audio/AudioPlaybackCapture.java | 9 +++++++- 12 files changed, 61 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index d5f129d0..e0928cbd 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -6,6 +6,7 @@ _scrcpy() { --audio-buffer= --audio-codec= --audio-codec-options= + --audio-dup --audio-encoder= --audio-source= --audio-output-buffer= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c49c24eb..0f06ba4b 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -13,6 +13,7 @@ arguments=( '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' + '--audio-dup=[Duplicate audio]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' '--audio-source=[Select the audio source]:source:(output mic playback)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 19b4ab6b..de2b8ac6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -49,6 +49,12 @@ The list of possible codec options is available in the Android documentation: +.TP +.B \-\-audio\-dup +Duplicate audio (capture and keep playing on the device). + +This feature is only available with --audio-source=playback. + .TP .BI "\-\-audio\-encoder " name Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). diff --git a/app/src/cli.c b/app/src/cli.c index 1699e46d..1792384e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -100,6 +100,7 @@ enum { OPT_NO_WINDOW, OPT_MOUSE_BIND, OPT_NO_MOUSE_HOVER, + OPT_AUDIO_DUP, }; struct sc_option { @@ -177,6 +178,13 @@ static const struct sc_option options[] = { "Android documentation: " "", }, + { + .longopt_id = OPT_AUDIO_DUP, + .longopt = "audio-dup", + .text = "Duplicate audio (capture and keep playing on the device).\n" + "This feature is only available with --audio-source=playback." + + }, { .longopt_id = OPT_AUDIO_ENCODER, .longopt = "audio-encoder", @@ -2615,6 +2623,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_WINDOW: opts->window = false; break; + case OPT_AUDIO_DUP: + opts->audio_dup = true; + break; default: // getopt prints the error message on stderr return false; @@ -2891,6 +2902,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->audio_dup) { + if (!opts->audio) { + LOGE("--audio-dup not supported if audio is disabled"); + return false; + } + + if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) { + LOGE("--audio-dup is specific to --audio-source=playback"); + return false; + } + } + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; diff --git a/app/src/options.c b/app/src/options.c index 5eec6427..6fca6ad5 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -101,6 +101,7 @@ const struct scrcpy_options scrcpy_options_default = { .list = 0, .window = true, .mouse_hover = true, + .audio_dup = false, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 403685e3..140d12b1 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -297,6 +297,7 @@ struct scrcpy_options { uint8_t list; bool window; bool mouse_hover; + bool audio_dup; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 376d5839..43864661 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -394,6 +394,7 @@ scrcpy(struct scrcpy_options *options) { .display_id = options->display_id, .video = options->video, .audio = options->audio, + .audio_dup = options->audio_dup, .show_touches = options->show_touches, .stay_awake = options->stay_awake, .video_codec_options = options->video_codec_options, diff --git a/app/src/server.c b/app/src/server.c index e32aa556..0db29183 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -292,6 +292,9 @@ execute_server(struct sc_server *server, ADD_PARAM("audio_source=%s", sc_server_get_audio_source_name(params->audio_source)); } + if (params->audio_dup) { + ADD_PARAM("audio_dup=true"); + } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); } diff --git a/app/src/server.h b/app/src/server.h index 062af0a9..cffa510e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -50,6 +50,7 @@ struct sc_server_params { uint32_t display_id; bool video; bool audio; + bool audio_dup; bool show_touches; bool stay_awake; bool force_adb_forward; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 143fbb9a..2f86d8ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -26,6 +26,7 @@ public class Options { private AudioCodec audioCodec = AudioCodec.OPUS; private VideoSource videoSource = VideoSource.DISPLAY; private AudioSource audioSource = AudioSource.OUTPUT; + private boolean audioDup; private int videoBitRate = 8000000; private int audioBitRate = 128000; private int maxFps; @@ -100,6 +101,10 @@ public class Options { return audioSource; } + public boolean getAudioDup() { + return audioDup; + } + public int getVideoBitRate() { return videoBitRate; } @@ -303,6 +308,9 @@ public class Options { } options.audioSource = audioSource; break; + case "audio_dup": + options.audioDup = Boolean.parseBoolean(value); + break; case "max_size": options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8 break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 75d9ea15..11429e6c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -167,7 +167,13 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); AudioSource audioSource = options.getAudioSource(); - AudioCapture audioCapture = audioSource.isDirect() ? new AudioDirectCapture(audioSource) : new AudioPlaybackCapture(); + AudioCapture audioCapture; + if (audioSource.isDirect()) { + audioCapture = new AudioDirectCapture(audioSource); + } else { + audioCapture = new AudioPlaybackCapture(options.getAudioDup()); + } + Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java index 2a0d23fb..e38493f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -18,9 +18,15 @@ import java.nio.ByteBuffer; public final class AudioPlaybackCapture implements AudioCapture { + private final boolean keepPlayingOnDevice; + private AudioRecord recorder; private AudioRecordReader reader; + public AudioPlaybackCapture(boolean keepPlayingOnDevice) { + this.keepPlayingOnDevice = keepPlayingOnDevice; + } + @SuppressLint("PrivateApi") private AudioRecord createAudioRecord() throws AudioCaptureException { // See @@ -60,7 +66,8 @@ public final class AudioPlaybackCapture implements AudioCapture { Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class); setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat()); - int routeFlags = audioMixClass.getField("ROUTE_FLAG_LOOP_BACK").getInt(null); + String routeFlagName = keepPlayingOnDevice ? "ROUTE_FLAG_LOOP_BACK_RENDER" : "ROUTE_FLAG_LOOP_BACK"; + int routeFlags = audioMixClass.getField(routeFlagName).getInt(null); // audioMixBuilder.setRouteFlags(routeFlag); Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class); From 127a271d34816f4f06cb7dbcba2b939f24c1ca6a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 20:59:39 +0200 Subject: [PATCH 32/45] Switch audio source if audio-dup is set Automatically switch implicit audio source to "playback" if --audio-dup is passed. This allows to run: scrcpy --audio-dup without specifying explicitly: scrcpy --audio-source=playback --audio-dup PR #5102 --- app/src/cli.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 1792384e..dd1b6799 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2895,7 +2895,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { // Select the audio source according to the video source if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { - opts->audio_source = SC_AUDIO_SOURCE_OUTPUT; + if (opts->audio_dup) { + LOGI("Audio duplication enabled: audio source switched to " + "\"playback\""); + opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK; + } else { + opts->audio_source = SC_AUDIO_SOURCE_OUTPUT; + } } else { opts->audio_source = SC_AUDIO_SOURCE_MIC; LOGI("Camera video source: microphone audio source selected"); From ed4066902d08e4abb91b4b6c7d828f190ab65bfb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 21:15:03 +0200 Subject: [PATCH 33/45] Update documentation for audio playback capture PR #5102 --- doc/audio.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc/audio.md b/doc/audio.md index 0c0409a9..750163e0 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -66,6 +66,30 @@ the computer: scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ``` +### Duplication + +An alternative device audio capture method is also available (only for Android +13 and above): + +``` +scrcpy --audio-source=playback +``` + +This audio source supports keeping the audio playing on the device while +mirroring, with `--audio-dup`: + +```bash +scrcpy --audio-source=playback --audio-dup +# or simply: +scrcpy --audio-dup # --audio-source=playback is implied +``` + +However, it requires Android 13, and Android apps can opt-out (so they are not +captured). + + +See [#4380](https://github.com/Genymobile/scrcpy/issues/4380). + ## Codec From 65bd6bd8d4cd16baf44a80911b67afcfa8615106 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Jul 2024 17:51:50 +0200 Subject: [PATCH 34/45] Explicitly accept issues for general questions Add an empty question template, and reword the "Contact" section in the README. Refs #5117 --- .github/ISSUE_TEMPLATE/question.md | 8 ++++++++ README.md | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..14dc373a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,8 @@ +--- +name: Question +about: Ask a question about scrcpy +title: '' +labels: '' +assignees: '' + +--- diff --git a/README.md b/README.md index 3185652b..36c8c272 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,14 @@ documented in the following pages: ## Contact -If you encounter a bug, please read the [FAQ](FAQ.md) first, then open an [issue]. +You can open an [issue] for bug reports, feature requests or general questions. + +For bug reports, please read the [FAQ](FAQ.md) first, you might find a solution +to your problem immediately. [issue]: https://github.com/Genymobile/scrcpy/issues -For general questions or discussions, you can also use: +You can also use: - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) From bbfac9ae1fba08a045557abe9612703ace8a3890 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Jul 2024 17:56:26 +0200 Subject: [PATCH 35/45] Add FUNDING.yml The donation links were already in the README. Also add them in the format expected by GitHub in FUNDING.yml. --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..b567129a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: [rom1v] +liberapay: rom1v +custom: ["https://paypal.me/rom2v"] From 071d459ad7d32fb6465216a0f5df7696db3adc02 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Jul 2024 19:58:40 +0200 Subject: [PATCH 36/45] Fix --no-audio By default, the audio source is initialized to SC_AUDIO_SOURCE_AUTO, and is "resolved" only if audio is enabled. But the server arguments were built assuming that the audio source was never SC_AUDIO_SOURCE_AUTO (even with audio disabled), causing a crash. Regression introduced by a10f8cd798023f858796b023cb846fa2184ad2c7. --- app/src/server.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 0db29183..41517f18 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -288,7 +288,9 @@ execute_server(struct sc_server *server, assert(params->video_source == SC_VIDEO_SOURCE_CAMERA); ADD_PARAM("video_source=camera"); } - if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) { + // If audio is enabled, an "auto" audio source must have been resolved + assert(params->audio_source != SC_AUDIO_SOURCE_AUTO || !params->audio); + if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT && params->audio) { ADD_PARAM("audio_source=%s", sc_server_get_audio_source_name(params->audio_source)); } From f691ebb1b4fde44789292d1e8ed41adaee8e6854 Mon Sep 17 00:00:00 2001 From: Al Grimes Date: Sun, 28 Jul 2024 13:01:06 +0100 Subject: [PATCH 37/45] Add workaround for TCL Android 12 Smart TVs Fixes #5140 PR #5148 Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 6a4a57e1..fa09d88d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -66,11 +66,13 @@ public final class Workarounds { // - // - mustFillAppInfo = true; - } else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth")) { + } else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth") || Build.BRAND.equalsIgnoreCase("tcl")) { // More workarounds must be applied for Honor devices: // - - // and Skyworth devices: + // for Skyworth devices: // - + // and for TCL devices: + // - // // The system context must not be set for all devices, because it would cause other problems: // - From 2b6089cbfc29c41643e0c0e8049bda3ede777b61 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2024 00:43:03 +0200 Subject: [PATCH 38/45] Enable workarounds by default Workarounds were disabled by default, and only enabled for some devices or under specific conditions. But it seems they are needed for more and more devices, so enable them by default. They could be disabled for specific devices if necessary in the future. In the past, these workarounds caused a (harmless) exception to be printed on some Xiaomi devices [1]. But this is not a problem anymore since commit b8c5853aa6ac9cfbe3fb4e46bf10978b3fa212e3. They also caused problems for audio on Vivo devices [2], but it seems this is not the case anymore [3]. They might also impact an old Nvidia Shield [4], but hopefully this is fixed now. [1]: [2]: [3]: [4]: PR #5154 --- .../java/com/genymobile/scrcpy/Server.java | 4 +- .../com/genymobile/scrcpy/Workarounds.java | 58 ++----------------- 2 files changed, 7 insertions(+), 55 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 11429e6c..7817fdf5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -144,7 +144,7 @@ public final class Server { final Device device = camera ? null : new Device(options); - Workarounds.apply(audio, camera); + Workarounds.apply(); List asyncProcessors = new ArrayList<>(); @@ -279,7 +279,7 @@ public final class Server { Ln.i(LogUtils.buildDisplayListMessage()); } if (options.getListCameras() || options.getListCameraSizes()) { - Workarounds.apply(false, true); + Workarounds.apply(); Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); } // Just print the requested data, do not mirror diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index fa09d88d..8fc38555 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -51,66 +51,18 @@ public final class Workarounds { // not instantiable } - public static void apply(boolean audio, boolean camera) { - boolean mustFillConfigurationController = false; - boolean mustFillAppInfo = false; - boolean mustFillAppContext = false; - - if (Build.BRAND.equalsIgnoreCase("meizu")) { - // Workarounds must be applied for Meizu phones: - // - - // - - // - - // - // But only apply when strictly necessary, since workarounds can cause other issues: - // - - // - - mustFillAppInfo = true; - } else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth") || Build.BRAND.equalsIgnoreCase("tcl")) { - // More workarounds must be applied for Honor devices: - // - - // for Skyworth devices: - // - - // and for TCL devices: - // - - // - // The system context must not be set for all devices, because it would cause other problems: - // - - // - - mustFillAppInfo = true; - mustFillAppContext = true; - } - - if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - // Before Android 11, audio is not supported. - // Since Android 12, we can properly set a context on the AudioRecord. - // Only on Android 11 we must fill the application context for the AudioRecord to work. - mustFillAppContext = true; - } - - if (camera) { - mustFillAppInfo = true; - mustFillAppContext = true; - } - + public static void apply() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), // which requires a non-null ConfigurationController. // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. // - mustFillConfigurationController = true; - } - - if (mustFillConfigurationController) { - // Must be call before fillAppContext() because it is necessary to get a valid system context + // Must be called before fillAppContext() because it is necessary to get a valid system context. fillConfigurationController(); } - if (mustFillAppInfo) { - fillAppInfo(); - } - if (mustFillAppContext) { - fillAppContext(); - } + + fillAppInfo(); + fillAppContext(); } @SuppressWarnings("deprecation") From 5d2441d1983d2b20854817d55b45a565af16694f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2024 15:21:49 +0200 Subject: [PATCH 39/45] Upgrade SDL (2.30.5) for Windows --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 589f93e5..0a42bc1f 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=2.30.4 +VERSION=2.30.5 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=dcc2c8c9c3e9e1a7c8d61d9522f1cba4e9b740feb560dcb15234030984610ee2 +SHA256SUM=be3ca88f8c362704627a0bc5406edb2cd6cc6ba463596d81ebb7c2f18763d3bf cd "$SOURCES_DIR" From 52136268ef931e37f93cb05350479e386bb51fbf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Aug 2024 18:15:59 +0200 Subject: [PATCH 40/45] Bump version to 2.6 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 717d9cb2..926dd655 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.5" + VALUE "ProductVersion", "2.6" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 1d11e574..d4a1d6a0 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.5', + version: '2.6', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index d17ffcb2..177f74ab 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20500 - versionName "2.5" + versionCode 20600 + versionName "2.6" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 6dc547d9..f8eb0510 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.5 +SCRCPY_VERSION_NAME=2.6 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 67f93350f679e40ba979198753f4c657fe589c87 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Aug 2024 18:46:10 +0200 Subject: [PATCH 41/45] Update links to 2.6 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 36c8c272..25eeb96e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.5) +# scrcpy (v2.6) scrcpy diff --git a/doc/build.md b/doc/build.md index a35910f8..707b9064 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.5`][direct-scrcpy-server] - SHA-256: `1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15` + - [`scrcpy-server-v2.6`][direct-scrcpy-server] + SHA-256: `7b723ff79a27f14e6ebaaaae7ef9548c40651c94e64d178612b13adf7158eb2e` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-server-v2.6 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 139c3419..6842edc6 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.5.zip`][direct-win64] (64-bit) - SHA-256: `345cf04a66a9144281dce72ca4e82adfd2c3092463196e586051df4c69e1507b` - - [`scrcpy-win32-v2.5.zip`][direct-win32] (32-bit) - SHA-256: `d56312a92471565fa4f3a6b94e8eb07717c4c90f2c0f05b03ba444e1001806ec` + - [`scrcpy-win64-v2.6.zip`][direct-win64] (64-bit) + SHA-256: `3d490a72997af950aec0540e28627ada35c8226bc9774500014c9697d9b53194` + - [`scrcpy-win32-v2.6.zip`][direct-win32] (32-bit) + SHA-256: `6c68f6b31ddef5ed61a7546f423bd4fc99d568eb4c4e3409e0df496187eb3783` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win64-v2.5.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win32-v2.5.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-win64-v2.6.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-win32-v2.6.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 2bd6d7e6..9dbf3f73 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5 -PREBUILT_SERVER_SHA256=1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-server-v2.6 +PREBUILT_SERVER_SHA256=7b723ff79a27f14e6ebaaaae7ef9548c40651c94e64d178612b13adf7158eb2e echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 992b4922fe32fd5bf96d0df204300f7c35f120fe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Aug 2024 18:40:07 +0200 Subject: [PATCH 42/45] Document INJECT_EVENTS permission issue on Xiaomi Make explicit in the prerequisites the exact error message when "USB debugging (Security Settings)" is not set. --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 25eeb96e..d4844739 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,16 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s). [enable-adb]: https://developer.android.com/studio/debug/dev-options#enable -On some devices, you also need to enable [an additional option][control] `USB -debugging (Security Settings)` (this is an item different from `USB debugging`) -to control it using a keyboard and mouse. Rebooting the device is necessary once -this option is set. +On some devices (especially Xiaomi), you might get the following error: + +``` +java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. +``` + +In that case, you need to enable [an additional option][control] `USB debugging +(Security Settings)` (this is an item different from `USB debugging`) to control +it using a keyboard and mouse. Rebooting the device is necessary once this +option is set. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 From 773c23fda298fd3dace4ded5a89e8716d3c0fc76 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Aug 2024 20:20:12 +0200 Subject: [PATCH 43/45] Inject finger input whenever possible Even if the pointer is a mouse, inject it as a finger unless it is required to be a mouse, that is: - when it is a HOVER_MOUSE event, or - when a secondary button is pressed. Some apps/games only accept events from a finger/touchscreen, so using a mouse by default does not work for them. For simplicity, make this change on the server side just before event injection (so that the client does not need to know about this hacky behavior). Refs 6808288823239b0f3a76f9be377e4de82e91b35a Refs c7b1d0ea9af8bb9603ec8f713f1a5fbf3f628b6a Fixes #5162 Fixes #5163 --- .../main/java/com/genymobile/scrcpy/control/Controller.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 85425113..1494c10a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -278,8 +278,9 @@ public class Controller implements AsyncProcessor { pointer.setPressure(pressure); int source; - if (pointerId == POINTER_ID_MOUSE) { - // real mouse event + boolean activeSecondaryButtons = ((actionButton | buttons) & ~MotionEvent.BUTTON_PRIMARY) != 0; + if (pointerId == POINTER_ID_MOUSE && (action == MotionEvent.ACTION_HOVER_MOVE || activeSecondaryButtons)) { + // real mouse event, or event incompatible with a finger pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; source = InputDevice.SOURCE_MOUSE; pointer.setUp(buttons == 0); From cc41115625553d2f9ed8e96302174f189f2f40c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Aug 2024 22:32:04 +0200 Subject: [PATCH 44/45] Bump version to 2.6.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 926dd655..9e0d90c2 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.6" + VALUE "ProductVersion", "2.6.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index d4a1d6a0..b532006a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.6', + version: '2.6.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 177f74ab..decacd3f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20600 - versionName "2.6" + versionCode 20601 + versionName "2.6.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index f8eb0510..5ee7af30 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.6 +SCRCPY_VERSION_NAME=2.6.1 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 44b3fd82b1831f4aa436268870adf32bddb81924 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Aug 2024 22:58:09 +0200 Subject: [PATCH 45/45] Update links to 2.6.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d4844739..67fdf364 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.6) +# scrcpy (v2.6.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 707b9064..15e0ffff 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.6`][direct-scrcpy-server] - SHA-256: `7b723ff79a27f14e6ebaaaae7ef9548c40651c94e64d178612b13adf7158eb2e` + - [`scrcpy-server-v2.6.1`][direct-scrcpy-server] + SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-server-v2.6 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 6842edc6..65ec2b45 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.6.zip`][direct-win64] (64-bit) - SHA-256: `3d490a72997af950aec0540e28627ada35c8226bc9774500014c9697d9b53194` - - [`scrcpy-win32-v2.6.zip`][direct-win32] (32-bit) - SHA-256: `6c68f6b31ddef5ed61a7546f423bd4fc99d568eb4c4e3409e0df496187eb3783` + - [`scrcpy-win64-v2.6.1.zip`][direct-win64] (64-bit) + SHA-256: `041fc3abf8578ddcead5a8c4a8be8960b7c4d45b21d3370ee2683605e86a728c` + - [`scrcpy-win32-v2.6.1.zip`][direct-win32] (32-bit) + SHA-256: `17a5d4d17230b4c90fad45af6395efda9aea287a03c04e6b4ecc9ceb8134ea04` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-win64-v2.6.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-win32-v2.6.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win64-v2.6.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win32-v2.6.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 9dbf3f73..2aad8cdc 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-server-v2.6 -PREBUILT_SERVER_SHA256=7b723ff79a27f14e6ebaaaae7ef9548c40651c94e64d178612b13adf7158eb2e +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 +PREBUILT_SERVER_SHA256=ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server