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"] 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..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.5) +# scrcpy (v2.6.1) scrcpy @@ -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 @@ -148,11 +154,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) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index b35ea5e4..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= @@ -111,7 +112,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..0f06ba4b 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -13,8 +13,9 @@ 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)' + '--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/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" diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 717d9cb2..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.5" + VALUE "ProductVersion", "2.6.1" END END BLOCK "VarFileInfo" diff --git a/app/scrcpy.1 b/app/scrcpy.1 index cf8dfa7f..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). @@ -57,7 +63,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. @@ -258,10 +270,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 +288,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..dd1b6799 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", @@ -189,7 +197,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.", }, { @@ -493,11 +507,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 +525,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', @@ -1924,7 +1945,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; } @@ -2095,24 +2122,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 +2457,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: @@ -2566,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; @@ -2701,26 +2761,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; } } @@ -2825,13 +2895,31 @@ 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"); } } + 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/control_msg.c b/app/src/control_msg.c index b3da5fe5..9b0fab67 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -64,13 +64,11 @@ 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: - 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..80714096 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -18,12 +18,11 @@ // 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_FINGER UINT64_C(-3) enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, 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/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) { 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 43b10d2d..d3c94d03 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; @@ -87,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; @@ -375,9 +367,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 ? POINTER_ID_VIRTUAL_MOUSE - : POINTER_ID_VIRTUAL_FINGER; + 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; @@ -662,12 +652,11 @@ 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->vfinger_down ? SC_POINTER_ID_GENERIC_FINGER + : 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); @@ -719,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: @@ -748,11 +737,25 @@ 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; + } + + 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: @@ -811,16 +814,23 @@ 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; + } + + 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 = im->has_secondary_click ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, - .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, - &im->mouse_bindings), + .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER + : SC_POINTER_ID_MOUSE, + .buttons_state = im->mouse_buttons_state, }; assert(im->mp->ops->process_mouse_click); @@ -846,14 +856,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. - const SDL_Keymod keymod = SDL_GetModState(); - const bool ctrl_pressed = keymod & KMOD_CTRL; - const 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))) { + if (change_vfinger) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); @@ -886,6 +889,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), @@ -896,8 +900,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 03c42fe6..88558549 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; @@ -33,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/options.c b/app/src/options.c index 5556d1f9..6fca6ad5 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 = { @@ -93,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 f840a989..140d12b1 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 { @@ -165,13 +166,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. @@ -291,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/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 5e78dbf3..43864661 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 @@ -389,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, @@ -567,7 +573,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, @@ -730,23 +736,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); } } diff --git a/app/src/server.c b/app/src/server.c index 4d55e994..41517f18 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,22 @@ 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; + } +} + +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; } } @@ -271,8 +288,14 @@ 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 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)); + } + 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/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); 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 diff --git a/doc/build.md b/doc/build.md index a35910f8..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.5`][direct-scrcpy-server] - SHA-256: `1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15` + - [`scrcpy-server-v2.6.1`][direct-scrcpy-server] + SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b` -[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.1/scrcpy-server-v2.6.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: 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 ``` diff --git a/doc/windows.md b/doc/windows.md index 139c3419..65ec2b45 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.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.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.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 2bd6d7e6..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.5/scrcpy-server-v2.5 -PREBUILT_SERVER_SHA256=1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15 +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 diff --git a/meson.build b/meson.build index 1d11e574..b532006a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.5', + version: '2.6.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', 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) diff --git a/server/build.gradle b/server/build.gradle index d17ffcb2..decacd3f 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 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 74bbd8ae..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.5 +SCRCPY_VERSION_NAME=2.6.1 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} @@ -50,14 +50,29 @@ 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/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 \ +) + +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 +83,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 +95,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" diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java b/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java deleted file mode 100644 index baa7d846..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.genymobile.scrcpy; - -/** - * 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/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/AudioSource.java deleted file mode 100644 index 466ea297..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.genymobile.scrcpy; - -import android.media.MediaRecorder; - -public enum AudioSource { - OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), - MIC("mic", MediaRecorder.AudioSource.MIC); - - private final String name; - private final int value; - - AudioSource(String name, int value) { - this.name = name; - this.value = value; - } - - int value() { - return value; - } - - static AudioSource findByName(String name) { - for (AudioSource audioSource : AudioSource.values()) { - if (name.equals(audioSource.name)) { - return audioSource; - } - } - - return null; - } -} 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..2f86d8ce 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; @@ -16,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; @@ -90,6 +101,10 @@ public class Options { return audioSource; } + public boolean getAudioDup() { + return audioDup; + } + public int getVideoBitRate() { return videoBitRate; } @@ -293,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 587a46df..7817fdf5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,5 +1,29 @@ 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.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; +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; @@ -120,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<>(); @@ -142,7 +166,14 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); - AudioCapture audioCapture = new AudioCapture(options.getAudioSource()); + AudioSource audioSource = options.getAudioSource(); + 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) { @@ -248,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 448e7099..8fc38555 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,5 +1,8 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.audio.AudioCaptureException; +import com.genymobile.scrcpy.util.Ln; + import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Application; @@ -48,62 +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")) { - // More workarounds must be applied for Honor 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") @@ -191,7 +150,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 @@ -332,8 +292,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 new file mode 100644 index 00000000..62903f83 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -0,0 +1,20 @@ +package com.genymobile.scrcpy.audio; + +import android.media.MediaCodec; + +import java.nio.ByteBuffer; + +public interface AudioCapture { + void checkCompatibility() throws AudioCaptureException; + void start() throws AudioCaptureException; + void stop(); + + /** + * 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/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/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/audio/AudioConfig.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java new file mode 100644 index 00000000..c77165a7 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java @@ -0,0 +1,29 @@ +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 + } + + public static AudioFormat createAudioFormat() { + AudioFormat.Builder builder = new AudioFormat.Builder(); + builder.setEncoding(ENCODING); + builder.setSampleRate(SAMPLE_RATE); + builder.setChannelMask(CHANNEL_CONFIG); + return builder.build(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java similarity index 55% rename from server/src/main/java/com/genymobile/scrcpy/AudioCapture.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 3934ad49..361c7bac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -1,55 +1,48 @@ -package com.genymobile.scrcpy; +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.AudioFormat; import android.media.AudioRecord; -import android.media.AudioTimestamp; import android.media.MediaCodec; +import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; import java.nio.ByteBuffer; -public final class AudioCapture { +public class AudioDirectCapture implements 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; - - // 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 long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) + 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; - private final AudioTimestamp timestamp = new AudioTimestamp(); - private long previousRecorderTimestamp = -1; - private long previousPts = 0; - private long nextPts = 0; - - public AudioCapture(AudioSource audioSource) { - this.audioSource = audioSource.value(); + public AudioDirectCapture(AudioSource audioSource) { + this.audioSource = getAudioSourceValue(audioSource); } - private static AudioFormat createAudioFormat() { - AudioFormat.Builder builder = new AudioFormat.Builder(); - builder.setEncoding(ENCODING); - builder.setSampleRate(SAMPLE_RATE); - builder.setChannelMask(CHANNEL_CONFIG); - return builder.build(); + 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) @@ -61,7 +54,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); @@ -86,7 +79,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); @@ -98,7 +91,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..."); } @@ -106,7 +99,7 @@ public final class AudioCapture { } } - private void startRecording() { + private void startRecording() throws AudioCaptureException { try { recorder = createAudioRecord(audioSource); } catch (NullPointerException e) { @@ -116,9 +109,19 @@ public final class AudioCapture { recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); } recorder.startRecording(); + reader = new AudioRecordReader(recorder); } - public void start() throws AudioCaptureForegroundException { + @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 { @@ -131,6 +134,7 @@ public final class AudioCapture { } } + @Override public void stop() { if (recorder != null) { // Will call .stop() if necessary, without throwing an IllegalStateException @@ -138,42 +142,9 @@ public final class AudioCapture { } } + @Override @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/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java similarity index 93% 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..8230e054 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; @@ -34,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; @@ -122,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); @@ -166,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, AudioCaptureException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); @@ -177,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); 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..e38493f2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -0,0 +1,137 @@ +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 final boolean keepPlayingOnDevice; + + private AudioRecord recorder; + private AudioRecordReader reader; + + public AudioPlaybackCapture(boolean keepPlayingOnDevice) { + this.keepPlayingOnDevice = keepPlayingOnDevice; + } + + @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()); + + 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); + 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/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java similarity index 88% 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..323caae4 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; @@ -18,14 +23,14 @@ 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); 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 { @@ -64,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); 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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java new file mode 100644 index 00000000..6082f20e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -0,0 +1,27 @@ +package com.genymobile.scrcpy.audio; + +public enum AudioSource { + OUTPUT("output"), + MIC("mic"), + PLAYBACK("playback"); + + private final String name; + + AudioSource(String name) { + 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)) { + return audioSource; + } + } + + return null; + } +} 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 96% rename from server/src/main/java/com/genymobile/scrcpy/Controller.java rename to server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 87faf8ba..1494c10a 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; @@ -22,7 +28,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 +278,9 @@ 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) + 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); 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 94% 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..5a1083fd 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; @@ -319,10 +323,22 @@ 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.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/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 ed5c8d75..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; @@ -38,38 +38,61 @@ 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 + } + + 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; } @@ -78,27 +101,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; } @@ -120,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); } } 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..038e7ca0 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; @@ -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(); @@ -134,7 +143,7 @@ public final class SurfaceControl { return getPhysicalDisplayIdsMethod; } - public static boolean hasPhysicalDisplayIdsMethod() { + public static boolean hasGetPhysicalDisplayIdsMethod() { try { getGetPhysicalDisplayIdsMethod(); return true; 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;