diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1b69a0650f..a7e878a35a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -82,6 +82,18 @@ Start in fullscreen. .B \-h, \-\-help Print this help. +.TP +.B \-i, \-\-input\-mode mode +Select input mode for keyboard events. + +Possible values are "auto", "hid" and "inject". + +"auto" is default if not specified, which attemps "hid" first and will fallback to "inject" if failed. + +"hid" uses Android's USB HID over AoAv2 feature to simulate physical keyboard's events, which provides better experience for IME users if supported by you device. + +"inject" is the legacy scrcpy way by injecting keycode events on Android, works on most devices. + .TP .B \-\-legacy\-paste Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c index 5f65ca2940..9415038683 100644 --- a/app/src/aoa_hid.c +++ b/app/src/aoa_hid.c @@ -48,6 +48,10 @@ get_usb_serial(libusb_device *device, char *buffer, int size) { } libusb_device *aoa_find_usb_device(const char *serial) { + if (!serial) { + return NULL; + } + libusb_device **list; libusb_device *result = NULL; ssize_t count = libusb_get_device_list(NULL, &list); diff --git a/app/src/cli.c b/app/src/cli.c index d22096cafa..e2f8e8c91c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -79,6 +79,17 @@ scrcpy_print_usage(const char *arg0) { " -h, --help\n" " Print this help.\n" "\n" + " -i, --input-mode mode\n" + " Select input mode for keyboard events.\n" + " Possible values are \"auto\", \"hid\" and \"inject\".\n" + " \"auto\" is default if not specified, which attemps \"hid\"\n" + " first and will fallback to \"inject\" if failed.\n" + " \"hid\" uses Android's USB HID over AoAv2 feature to\n" + " simulate physical keyboard's events, which provides better\n" + " experience for IME users if supported by you device.\n" + " \"inject\" is the legacy scrcpy way by injecting keycode\n" + " events on Android, works on most devices.\n" + "\n" " --legacy-paste\n" " Inject computer clipboard text as a sequence of key events\n" " on Ctrl+v (like MOD+Shift+v).\n" @@ -673,6 +684,23 @@ parse_record_format(const char *optarg, enum sc_record_format *format) { return false; } +static bool +parse_input_mode(const char *optarg, enum sc_input_mode *input_mode) { + if (!strcmp(optarg, "auto")) { + *input_mode = SC_INPUT_MODE_AUTO; + return true; + } else if (!strcmp(optarg, "hid")) { + *input_mode = SC_INPUT_MODE_HID; + return true; + } else if (!strcmp(optarg, "inject")) { + *input_mode = SC_INPUT_MODE_INJECT; + return true; + } + LOGE("Unsupported input mode: %s (expected auto, hid or inject)", optarg); + return false; +} + + static enum sc_record_format guess_record_format(const char *filename) { size_t len = strlen(filename); @@ -738,6 +766,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_FORWARD_ALL_CLICKS}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, + {"input-mode", required_argument, NULL, 'i'}, {"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE}, {"lock-video-orientation", optional_argument, NULL, OPT_LOCK_VIDEO_ORIENTATION}, @@ -784,7 +813,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { optind = 0; // reset to start from the first argument in tests int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w", + while ((c = getopt_long(argc, argv, "b:c:fF:hi:m:nNp:r:s:StTvV:w", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -817,6 +846,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 'h': args->help = true; break; + case 'i': + if (!parse_input_mode(optarg, &opts->input_mode)) { + return false; + } + break; case OPT_MAX_FPS: if (!parse_max_fps(optarg, &opts->max_fps)) { return false; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 1b01ea1bfe..7cf9dd507f 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -61,8 +61,8 @@ input_manager_init(struct input_manager *im, struct controller *controller, im->screen = screen; im->repeat = 0; - im->usb_handle = NULL; im->use_hid_over_aoa = false; + im->usb_handle = NULL; im->control = options->control; im->forward_key_repeat = options->forward_key_repeat; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 79c423c7f2..1c04342a78 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -17,8 +17,8 @@ struct input_manager { struct controller *controller; struct screen *screen; - libusb_device_handle *usb_handle; bool use_hid_over_aoa; + libusb_device_handle *usb_handle; // SDL reports repeated events as a boolean, but Android expects the actual // number of repetitions. This variable keeps track of the count. diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 393c1ccaa5..f5d905a9c5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -416,64 +416,87 @@ scrcpy(const struct scrcpy_options *options) { input_manager_init(&s->input_manager, &s->controller, &s->screen, options); + bool use_hid_over_aoa = false; libusb_device_handle *usb_handle = NULL; libusb_device *usb_device = NULL; - if (options->serial) { - LOGD("USB serial provided, starting in HID over AoAv2 mode"); - s->input_manager.use_hid_over_aoa = true; + if (options->input_mode != SC_INPUT_MODE_INJECT) { + LOGD("Starting in HID over AoAv2 mode"); + use_hid_over_aoa = true; + } else { + LOGD("Starting in inject mode because of --input-mode=inject"); + use_hid_over_aoa = false; + } + + if (use_hid_over_aoa) { libusb_init(NULL); usb_device = aoa_find_usb_device(options->serial); if (!usb_device) { LOGW("USB device of serial %s not found", options->serial); - LOGW("Fallback to inject mode"); - s->input_manager.use_hid_over_aoa = false; + if (options->input_mode == SC_INPUT_MODE_AUTO) { + LOGW("Fallback to inject mode"); + use_hid_over_aoa = false; + } else { + LOGE("Remove --input-mode=hid from parameters to allow input mode fallback"); + goto end; + } } } - if (s->input_manager.use_hid_over_aoa && - aoa_open_usb_handle(usb_device, &usb_handle) < 0) { + if (use_hid_over_aoa && aoa_open_usb_handle(usb_device, &usb_handle) < 0) { LOGW("Open USB handle failed"); - LOGW("Fallback to inject mode"); libusb_unref_device(usb_device); - s->input_manager.use_hid_over_aoa = false; + if (options->input_mode == SC_INPUT_MODE_AUTO) { + LOGW("Fallback to inject mode"); + use_hid_over_aoa = false; + } else { + LOGE("Remove --input-mode=hid from parameters to allow input mode fallback"); + goto end; + } } - if (s->input_manager.use_hid_over_aoa && - aoa_register_hid(usb_handle, REPORT_DESC.size) < 0) { + if (use_hid_over_aoa && aoa_register_hid(usb_handle, REPORT_DESC.size) < 0) { LOGW("Register HID failed"); - LOGW("Fallback to inject mode"); libusb_close(usb_handle); libusb_unref_device(usb_device); - s->input_manager.use_hid_over_aoa = false; + if (options->input_mode == SC_INPUT_MODE_AUTO) { + LOGW("Fallback to inject mode"); + use_hid_over_aoa = false; + } else { + LOGE("Remove --input-mode=hid from parameters to allow input mode fallback"); + goto end; + } } struct libusb_device_descriptor desc; // Make sure usb_device is found. - if (s->input_manager.use_hid_over_aoa) { + if (use_hid_over_aoa) { libusb_get_device_descriptor(usb_device, &desc); } - - if (s->input_manager.use_hid_over_aoa && - aoa_set_hid_report_desc(usb_handle, &REPORT_DESC, - desc.bMaxPacketSize0) < 0) { + if (use_hid_over_aoa && aoa_set_hid_report_desc(usb_handle, &REPORT_DESC, + desc.bMaxPacketSize0) < 0) { LOGW("Set HID report desc failed"); - LOGW("Fallback to inject mode"); libusb_close(usb_handle); libusb_unref_device(usb_device); - s->input_manager.use_hid_over_aoa = false; + if (options->input_mode == SC_INPUT_MODE_AUTO) { + LOGW("Fallback to inject mode"); + use_hid_over_aoa = false; + } else { + LOGE("Remove --input-mode=hid from parameters to allow input mode fallback"); + goto end; + } } // Finally successfully set up HID over AoAv2. - if (s->input_manager.use_hid_over_aoa) { + if (use_hid_over_aoa) { LOGD("Successfully set up HID over AoAv2 mode"); + s->input_manager.use_hid_over_aoa = use_hid_over_aoa; s->input_manager.usb_handle = usb_handle; + // Events sent too early after setting the HID descriptor + // may leads into error. + usleep(1000000); } - // Events sent too early after setting the HID descriptor - // may leads into error. - usleep(1000000); - ret = event_loop(s, options); LOGD("quit..."); @@ -481,13 +504,14 @@ scrcpy(const struct scrcpy_options *options) { // only be called once the stream thread is joined (it may take time) screen_hide_window(&s->screen); -end: // Close HID over AoAv2 so the soft keyboard shows again on Android. - if (s->input_manager.use_hid_over_aoa) { + if (use_hid_over_aoa) { aoa_unregister_hid(usb_handle); libusb_close(usb_handle); libusb_unref_device(usb_device); } + +end: // The stream is not stopped explicitly, because it will stop by itself on // end-of-stream if (controller_started) { diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8b76fb25a2..27abe517ef 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -44,6 +44,12 @@ enum sc_shortcut_mod { SC_MOD_RSUPER = 1 << 5, }; +enum sc_input_mode { + SC_INPUT_MODE_AUTO, + SC_INPUT_MODE_HID, + SC_INPUT_MODE_INJECT +}; + struct sc_shortcut_mods { unsigned data[SC_MAX_SHORTCUT_MODS]; unsigned count; @@ -68,6 +74,7 @@ struct scrcpy_options { const char *v4l2_device; enum sc_log_level log_level; enum sc_record_format record_format; + enum sc_input_mode input_mode; struct sc_port_range port_range; struct sc_shortcut_mods shortcut_mods; uint16_t max_size; @@ -112,6 +119,7 @@ struct scrcpy_options { .v4l2_device = NULL, \ .log_level = SC_LOG_LEVEL_INFO, \ .record_format = SC_RECORD_FORMAT_AUTO, \ + .input_mode = SC_INPUT_MODE_AUTO, \ .port_range = { \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \