diff --git a/BUILD.md b/BUILD.md index 87078b715b..864de36e8c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -88,11 +88,12 @@ Install the required packages from your package manager. ```bash # runtime dependencies -sudo apt install ffmpeg libsdl2-2.0-0 adb +sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0 # client build dependencies sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ - libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev + libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev\ + libusb-dev # server build dependencies sudo apt install openjdk-11-jdk @@ -114,7 +115,7 @@ pip3 install meson sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies -sudo dnf install SDL2-devel ffms2-devel meson gcc make +sudo dnf install SDL2-devel ffms2-devel libusb-devel meson gcc make # server build dependencies sudo dnf install java-devel @@ -159,7 +160,8 @@ install the required packages: ```bash # runtime dependencies pacman -S mingw-w64-x86_64-SDL2 \ - mingw-w64-x86_64-ffmpeg + mingw-w64-x86_64-ffmpeg \ + mingw-w64-x86_64-libusb # client build dependencies pacman -S mingw-w64-x86_64-make \ @@ -173,7 +175,8 @@ For a 32 bits version, replace `x86_64` by `i686`: ```bash # runtime dependencies pacman -S mingw-w64-i686-SDL2 \ - mingw-w64-i686-ffmpeg + mingw-w64-i686-ffmpeg \ + mingw-w64-i686-libusb # client build dependencies pacman -S mingw-w64-i686-make \ @@ -197,7 +200,7 @@ Install the packages with [Homebrew]: ```bash # runtime dependencies -brew install sdl2 ffmpeg +brew install sdl2 ffmpeg libusb # client build dependencies brew install pkg-config meson diff --git a/README.md b/README.md index 7b1d2e782a..c1ffbe373d 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,18 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) If `--max-size` is also specified, resizing is applied after cropping. +#### USB HID over AoAv2 + +Scrcpy can simulate a USB physical keyboard on Android to provide better input +experience, to use this feature you need to find your Android device's USB ID. +Then run it with `--usb` argument. + +```bash +scrcpy --usb 04e8:6861 +``` + +USB ID can be found via lsusb. It will fallback to inject mode if HID over AoAv2 +failed to set up. #### Lock video orientation diff --git a/app/meson.build b/app/meson.build index f5345803cc..538d5a40a9 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,6 +1,7 @@ src = [ 'src/main.c', 'src/adb.c', + 'src/aoa_hid.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', @@ -12,6 +13,7 @@ src = [ 'src/file_handler.c', 'src/fps_counter.c', 'src/frame_buffer.c', + 'src/hid_keyboard.c', 'src/input_manager.c', 'src/opengl.c', 'src/receiver.c', @@ -54,6 +56,7 @@ if not get_option('crossbuild_windows') dependency('libavformat'), dependency('libavcodec'), dependency('libavutil'), + dependency('libusb-1.0'), dependency('sdl2'), ] @@ -62,7 +65,7 @@ if not get_option('crossbuild_windows') endif else - + # TODO: add libusb dependency for windows. # cross-compile mingw32 build (from Linux to Windows) prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1b69a0650f..a99723ea38 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -106,6 +106,12 @@ Limit both the width and height of the video to \fIvalue\fR. The other dimension Default is 0 (unlimited). +.TP +.BI "\-u, \-\-usb " vid:pid +Specify the USB ID of Android device to simulate a physical keyboard via HID over AoAv2, provide better input experience than inject mode. + +The USB ID of Android device can be found via lsusb. + .TP .B \-n, \-\-no\-control Disable device control (mirror the device in read\-only). diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c new file mode 100644 index 0000000000..2ca7bd9fab --- /dev/null +++ b/app/src/aoa_hid.c @@ -0,0 +1,142 @@ +#include "util/log.h" +#include "aoa_hid.h" + +// See . +#define ACCESSORY_REGISTER_HID 54 +#define ACCESSORY_UNREGISTER_HID 55 +#define ACCESSORY_SET_HID_REPORT_DESC 56 +#define ACCESSORY_SEND_HID_EVENT 57 + +#define DEFAULT_TIMEOUT 1000 + +inline static void print_libusb_error(enum libusb_error errcode) { + LOGW("libusb error: %s", libusb_strerror(errcode)); +} + +libusb_device *aoa_find_usb_device(uint16_t vid, uint16_t pid) { + libusb_device **list; + libusb_device *result = NULL; + ssize_t count = libusb_get_device_list(NULL, &list); + if (count < 0) { + print_libusb_error((enum libusb_error)count); + return NULL; + } + for (ssize_t i = 0; i < count; ++i) { + libusb_device *device = list[i]; + struct libusb_device_descriptor desc; + libusb_get_device_descriptor(device, &desc); + if (vid == desc.idVendor && pid == desc.idProduct) { + result = libusb_ref_device(device); + break; + } + } + libusb_free_device_list(list, 1); + return result; +} + +int aoa_open_usb_handle(libusb_device *device, libusb_device_handle **handle) { + int result = libusb_open(device, handle); + if (result < 0) { + print_libusb_error((enum libusb_error)result); + return result; + } + return 0; +} + +int aoa_register_hid(libusb_device_handle *handle, uint16_t report_desc_size) { + const uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + const uint8_t request = ACCESSORY_REGISTER_HID; + // See . + // value (arg0): accessory assigned ID for the HID device + // index (arg1): total length of the HID report descriptor + const uint16_t value = 0; + const uint16_t index = report_desc_size; + unsigned char *buffer = NULL; + const uint16_t length = 0; + int result = libusb_control_transfer(handle, request_type, request, value, + index, buffer, length, DEFAULT_TIMEOUT); + if (result < 0) { + print_libusb_error((enum libusb_error)result); + return result; + } + return 0; +} + +int aoa_unregister_hid(libusb_device_handle *handle) { + const uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + const uint8_t request = ACCESSORY_UNREGISTER_HID; + // See . + // value (arg0): accessory assigned ID for the HID device + // index (arg1): 0 + const uint16_t value = 0; + const uint16_t index = 0; + unsigned char *buffer = NULL; + const uint16_t length = 0; + int result = libusb_control_transfer(handle, request_type, request, value, + index, buffer, length, DEFAULT_TIMEOUT); + if (result < 0) { + print_libusb_error((enum libusb_error)result); + return result; + } + return 0; +} + +int +aoa_set_hid_report_desc(libusb_device_handle *handle, + const unsigned char *report_desc, uint16_t report_desc_size, + uint8_t max_packet_size_0) { + const uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + const uint8_t request = ACCESSORY_SET_HID_REPORT_DESC; + // See . + // value (arg0): accessory assigned ID for the HID device + const uint16_t value = 0; + // libusb_control_transfer expects non-const but should not modify it. + unsigned char *buffer = (unsigned char *)report_desc; + const uint16_t size = report_desc_size; + /** + * If the HID descriptor is longer than the endpoint zero max packet size, + * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC + * commands. The data for the descriptor must be sent sequentially + * if multiple packets are needed. + * + * See . + */ + // index (arg1): offset of data (buffer) in descriptor + uint16_t offset = 0; + while (offset < size) { + uint16_t packet_length = size - offset; + if (packet_length > max_packet_size_0) { + packet_length = max_packet_size_0; + } + int result = libusb_control_transfer( + handle, request_type, request, value, offset, buffer + offset, packet_length, DEFAULT_TIMEOUT); + offset += packet_length; + if (result < 0) { + print_libusb_error((enum libusb_error)result); + return result; + } + } + return 0; +} + +int +aoa_send_hid_event(libusb_device_handle *handle, + const unsigned char *event, uint16_t size) { + const uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + const uint8_t request = ACCESSORY_SEND_HID_EVENT; + // See . + // value (arg0): accessory assigned ID for the HID device + // index (arg1): 0 (unused) + const uint16_t value = 0; + const uint16_t index = 0; + // libusb_control_transfer expects non-const but should not modify it. + unsigned char *buffer = (unsigned char *)event; + const uint16_t length = size; + int result = libusb_control_transfer( + handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); + if (result < 0) { + print_libusb_error((enum libusb_error)result); + return result; + } + return 0; +} diff --git a/app/src/aoa_hid.h b/app/src/aoa_hid.h new file mode 100644 index 0000000000..a2e0bc30d3 --- /dev/null +++ b/app/src/aoa_hid.h @@ -0,0 +1,20 @@ +#ifndef AOA_HID_H +#define AOA_HID_H + +#include + +#include + +libusb_device *aoa_find_usb_device(uint16_t vid, uint16_t pid); +int aoa_open_usb_handle(libusb_device *device, libusb_device_handle **handle); +int aoa_register_hid(libusb_device_handle *handle, uint16_t report_desc_size); +int aoa_unregister_hid(libusb_device_handle *handle); +int +aoa_set_hid_report_desc(libusb_device_handle *handle, + const unsigned char *report_desc, uint16_t report_desc_size, + uint8_t max_packet_size_0); +int +aoa_send_hid_event(libusb_device_handle *handle, + const unsigned char *event, uint16_t size); + +#endif diff --git a/app/src/cli.c b/app/src/cli.c index d22096cafa..fbad6d6ec9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -105,6 +105,12 @@ scrcpy_print_usage(const char *arg0) { " is preserved.\n" " Default is 0 (unlimited).\n" "\n" + " -u, --usb vid:pid\n" + " Specify the USB ID of Android device to simulate a physical\n" + " keyboard via HID over AoAv2, provide better input experience\n" + " than inject mode.\n" + " The USB ID of Android device can be found via lsusb.\n" + "\n" " -n, --no-control\n" " Disable device control (mirror the device in read-only).\n" "\n" @@ -531,6 +537,29 @@ parse_display_id(const char *s, uint32_t *display_id) { return true; } +static bool +parse_usb_id(char *s, uint16_t *vid, uint16_t *pid) { + if (strlen(s) == 0) { + LOGE("USB ID parameter is empty"); + return false; + } + uint32_t v; + uint32_t p; + int result = sscanf(s, "%x:%x", &v, &p); + if (result != 2) { + LOGE("Invalid USB ID: %s", s); + return false; + } + if ((v & ~0xffff) || (p & ~0xffff)) { + LOGE("USB ID out of range: %s", s); + return false; + } + + *vid = (uint16_t)v; + *pid = (uint16_t)p; + return true; +} + static bool parse_log_level(const char *s, enum sc_log_level *log_level) { if (!strcmp(s, "verbose")) { @@ -743,6 +772,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_LOCK_VIDEO_ORIENTATION}, {"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"max-size", required_argument, NULL, 'm'}, + {"usb", required_argument, NULL, 'u'}, {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, {"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT}, @@ -784,7 +814,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:hm:u:nNp:r:s:StTvV:w", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -827,6 +857,12 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case 'u': + if (!parse_usb_id(optarg, &opts->usb_vid, &opts->usb_pid)) { + return false; + } + opts->use_hid_over_aoa = true; + break; case OPT_LOCK_VIDEO_ORIENTATION: if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c new file mode 100644 index 0000000000..0004cddefb --- /dev/null +++ b/app/src/hid_keyboard.c @@ -0,0 +1,256 @@ +#include + +#include "util/log.h" +#include "hid_keyboard.h" + +/** + * For HID over AoA, we only need report descriptor. + * Normally a basic HID keyboard uses 8 bytes, + * `Modifier Reserved Key Key Key Key Key Key`. + * See Appendix B.1 Protocol 1 (Keyboard) and + * Appendix C: Keyboard Implementation in + * . + * But if we want to support media keys on keyboard, + * we need to use two reports, + * report id 1 and usage page key codes for basic keyboard, + * report id 2 and usage page consumer for media keys. + * See 8. Report Protocol in + * . + * The former byte is item type prefix, we only use short items here. + * For how to calculate an item, read 6.2.2 Report Descriptor in + * . + * For Consumer Page tags, see 15 Consumer Page (0x0C) in + * . + * If you changed REPORT_DESC, please also update REPORT_DESC_SIZE defined in + * hid_keyboard.h. + */ +const unsigned char REPORT_DESC[REPORT_DESC_SIZE] = { + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Keyboard) + 0x09, 0x06, + + // Collection (Application) + 0xA1, 0x01, + // Report ID (1) + 0x85, 0x01, + + // Report Size (1) + 0x75, 0x01, + // Report Count (8) + 0x95, 0x08, + // Usage Page (Keyboard) + 0x05, 0x07, + // Usage Minimum (224) + 0x19, 0xE0, + // Usage Maximum (231) + 0x29, 0xE7, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Input (Data, Variable, Absolute): Modifier byte + 0x81, 0x02, + + // Report Size (8) + 0x75, 0x08, + // Report Count (1) + 0x95, 0x01, + // Input (Constant): Reserved byte + 0x81, 0x01, + + // Report Size (1) + 0x75, 0x01, + // Report Count (5) + 0x95, 0x05, + // Usage Page (LEDs) + 0x05, 0x08, + // Usage Minimum (1) + 0x19, 0x01, + // Usage Maximum (5) + 0x29, 0x05, + // Output (Data, Variable, Absolute): LED report + 0x91, 0x02, + + // Report Size (3) + 0x75, 0x03, + // Report Count (1) + 0x95, 0x01, + // Output (Constant): LED report padding + 0x91, 0x01, + + // Report Size (8) + 0x75, 0x08, + // Report Count (6) + 0x95, 0x06, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum(255) + 0x25, 0x65, + // Usage Page (Key Codes) + 0x05, 0x07, + // Usage Minimum (0) + 0x19, 0x00, + // Usage Maximum (255) + 0x29, 0x65, + // Input (Data, Array): Keys + 0x81, 0x00, + + // End Collection + 0xC0, + + // Usage Page (Consumer) + 0x05, 0x0C, + // Usage (Consumer Control) + 0x09, 0x01, + + // Collection (Application) + 0xA1, 0x01, + // Report ID (2) + 0x85, 0x02, + + // Report Size (1) + 0x75, 0x01, + // Report Count (8) + 0x95, 0x08, + // Usage Page (Consumer) + 0x05, 0x0C, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Usage (Scan Next Track) + 0x09, 0xB5, + // Usage (Scan Previous Track) + 0x09, 0xB6, + // Usage (Stop) + 0x09, 0xB7, + // Usage (Eject) + 0x09, 0xB8, + // Usage (Play/Pause) + 0x09, 0xCD, + // Usage (Mute) + 0x09, 0xE2, + // Usage (Volume Increment) + 0x09, 0xE9, + // Usage (Volume Decrement) + 0x09, 0xEA, + // Input (Data, Array) + 0x81, 0x02, + + // End Collection + 0xC0 +}; + +// See "SDL2/SDL_scancode.h". +// Maybe SDL_Keycode is used by most people, +// but SDL_Scancode is taken from USB HID protocol so I perfer this. +// 0xE0 is LeftControl, the start of modifiers and the end of keys. +#define KEYBOARD_KEYS 0xE0 +static bool keys[KEYBOARD_KEYS] = { false }; +static unsigned char modifiers = HID_KEYBOARD_MODIFIER_NONE; + +inline static unsigned char sdl_keymod_to_hid_modifiers(SDL_Keymod mod) { + unsigned char modifiers = HID_KEYBOARD_MODIFIER_NONE; + // That's why you should not write code drunked. + // SDL_Keymod: NONE NONE NONE NONE RGUI LGUI RALT LALT RCTRL LCTRL NONE NONE NONE NONE RSHIFT LSHIFT + // SDL hex: 0800 0400 0200 0100 0080 0040 0002 0001 + // HID modifiers: RGUI RALT RSHIFT RCTRL LGUI LALT LSHIFT LCTRL + // HID hex: 80 40 20 10 08 04 02 01 + modifiers |= ((mod & KMOD_LCTRL) >> 6); + modifiers |= ((mod & KMOD_LSHIFT) << 1); + modifiers |= ((mod & KMOD_LALT) >> 6); + modifiers |= ((mod & KMOD_LGUI) >> 7); + modifiers |= ((mod & KMOD_RCTRL) >> 3); + modifiers |= ((mod & KMOD_RSHIFT) << 4); + modifiers |= ((mod & KMOD_RALT) >> 3); + modifiers |= ((mod & KMOD_RGUI) >> 4); + return modifiers; +} + +// Pantom state is made of `ReportID, Modifiers, Reserved, ErrorRollOver, ErrorRollOver, ErrorRollOver, ErrorRollOver, ErrorRollOver, ErrorRollOver`. +static unsigned char phantom_state[HID_KEYBOARD_KEY_LENGTH] = { + HID_KEYBOARD_REPORT_ID, + HID_KEYBOARD_MODIFIER_NONE, + HID_KEYBOARD_RESERVED, + HID_KEYBOARD_ERROR_ROLL_OVER, + HID_KEYBOARD_ERROR_ROLL_OVER, + HID_KEYBOARD_ERROR_ROLL_OVER, + HID_KEYBOARD_ERROR_ROLL_OVER, + HID_KEYBOARD_ERROR_ROLL_OVER, + HID_KEYBOARD_ERROR_ROLL_OVER +}; +static unsigned char hid_keyboard_event[HID_KEYBOARD_KEY_LENGTH] = { + HID_KEYBOARD_REPORT_ID, + HID_KEYBOARD_MODIFIER_NONE, + HID_KEYBOARD_RESERVED, + HID_KEYBOARD_RESERVED, + HID_KEYBOARD_RESERVED, + HID_KEYBOARD_RESERVED, + HID_KEYBOARD_RESERVED, + HID_KEYBOARD_RESERVED, + HID_KEYBOARD_RESERVED +}; + +inline static void reset_hid_keyboard_event(void) { + hid_keyboard_event[HID_KEYBOARD_MODIFIER_INDEX] = HID_KEYBOARD_MODIFIER_NONE; + memset(hid_keyboard_event + HID_KEYBOARD_MODIFIER_INDEX + 2, + HID_KEYBOARD_RESERVED, HID_KEYBOARD_MAX_KEYS); +} + +void hid_keyboard_init(void) { + // Reset all states. + memset(keys, false, KEYBOARD_KEYS); + modifiers = HID_KEYBOARD_MODIFIER_NONE; + phantom_state[HID_KEYBOARD_MODIFIER_INDEX] = HID_KEYBOARD_MODIFIER_NONE; + reset_hid_keyboard_event(); +} + +void hid_keyboard_update_state(const SDL_KeyboardEvent *event) { + LOGD("Type: %s, Repeat: %s, Modifiers: %02x, Key: %02x", + event->type == SDL_KEYDOWN ? "down" : "up", + event->repeat != 0 ? "true" : "false", + sdl_keymod_to_hid_modifiers(event->keysym.mod), + event->keysym.scancode + ); + SDL_Scancode scancode = event->keysym.scancode; + // SDL also generates event when only modifiers are pressed, + // we cannot ignore them totally, for example press `a` first then + // press `Control`, if we ignore `Control` event, only `a` is sent. + if (scancode < KEYBOARD_KEYS) { + // Pressed is true and released is false. + keys[scancode] = (event->type == SDL_KEYDOWN); + LOGD("keys[%02x] = %s", scancode, keys[scancode] ? "true" : "false"); + } + // TODO: Maybe also handle media keys here? + modifiers = sdl_keymod_to_hid_modifiers(event->keysym.mod); + return; +} + +const unsigned char *hid_keyboard_get_hid_event(void) { + // Re-calculate pressed keys every time. + reset_hid_keyboard_event(); + int keys_pressed_count = 0; + for (int i = 0; i < KEYBOARD_KEYS; ++i) { + // USB HID protocol says that if keys exceeds report count, + // a phantom state should be report. + if (keys_pressed_count > HID_KEYBOARD_MAX_KEYS) { + // But the modifiers should be report normally for phantom state. + phantom_state[HID_KEYBOARD_MODIFIER_INDEX] = modifiers; + return phantom_state; + } + if (keys[i]) { + hid_keyboard_event[ + HID_KEYBOARD_MODIFIER_INDEX + 2 + keys_pressed_count] = i; + ++keys_pressed_count; + } + } + hid_keyboard_event[HID_KEYBOARD_MODIFIER_INDEX] = modifiers; + LOGD( + "HID Keyboard Event: %02x %02x %02x %02x %02x %02x %02x %02x %02x", + hid_keyboard_event[0], hid_keyboard_event[1], hid_keyboard_event[2], + hid_keyboard_event[3], hid_keyboard_event[4], hid_keyboard_event[5], + hid_keyboard_event[6], hid_keyboard_event[7], hid_keyboard_event[8] + ); + return hid_keyboard_event; +} diff --git a/app/src/hid_keyboard.h b/app/src/hid_keyboard.h new file mode 100644 index 0000000000..abc1ea5b24 --- /dev/null +++ b/app/src/hid_keyboard.h @@ -0,0 +1,54 @@ +#ifndef HID_KEYBOARD_H +#define HID_KEYBOARD_H + +#include + +#define REPORT_DESC_SIZE 102 +extern const unsigned char REPORT_DESC[REPORT_DESC_SIZE]; + +/** + * Because of dual-report, when we send hid events, we need to add report id + * as prefix, so keyboard keys looks like + * `0x01 Modifier Reserved Key Key Key Key Key Key` and media keys looks like + * `0x02 MediaMask` (one key per bit for media keys). + */ +#define HID_REPORT_ID_INDEX 0 +#define HID_KEYBOARD_MODIFIER_INDEX (HID_REPORT_ID_INDEX + 1) +#define HID_KEYBOARD_MODIFIER_NONE 0x00 +#define HID_KEYBOARD_MODIFIER_LEFT_CONTROL (1 << 0) +#define HID_KEYBOARD_MODIFIER_LEFT_SHIFT (1 << 1) +#define HID_KEYBOARD_MODIFIER_LEFT_ALT (1 << 2) +#define HID_KEYBOARD_MODIFIER_LEFT_GUI (1 << 3) +#define HID_KEYBOARD_MODIFIER_RIGHT_CONTROL (1 << 4) +#define HID_KEYBOARD_MODIFIER_RIGHT_SHIFT (1 << 5) +#define HID_KEYBOARD_MODIFIER_RIGHT_ALT (1 << 6) +#define HID_KEYBOARD_MODIFIER_RIGHT_GUI (1 << 7) +// USB HID protocol says 6 keys in an event is the requirement for BIOS +// keyboard support, though OS could support more keys via modifying the report +// desc, I think 6 is enough for us. +#define HID_KEYBOARD_MAX_KEYS 6 +#define HID_KEYBOARD_KEY_LENGTH (3 + HID_KEYBOARD_MAX_KEYS) +#define HID_KEYBOARD_REPORT_ID 0x01 +#define HID_KEYBOARD_RESERVED 0x00 +#define HID_KEYBOARD_ERROR_ROLL_OVER 0x01 +#define HID_MEDIA_KEY_LENGTH 2 +#define HID_MEDIA_REPORT_ID 0x02 +/** + * Media keys handle as mask so we define them here. + * FIXME: Some functions already exists on keyboard, maybe media key is useless? + */ +#define HID_MEDIA_KEY_UNDEFINED 0x00 +#define HID_MEDIA_KEY_NEXT (1 << 0) +#define HID_MEDIA_KEY_PREVIOUS (1 << 1) +#define HID_MEDIA_KEY_STOP (1 << 2) +#define HID_MEDIA_KEY_EJECT (1 << 3) +#define HID_MEDIA_KEY_PLAY_PAUSE (1 << 4) +#define HID_MEDIA_KEY_MUTE (1 << 5) +#define HID_MEDIA_VOLUME_UP (1 << 6) +#define HID_MEDIA_VOLUME_DOWN (1 << 7) + +void hid_keyboard_init(void); +void hid_keyboard_update_state(const SDL_KeyboardEvent *event); +const unsigned char *hid_keyboard_get_hid_event(void); + +#endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index a5d0ad07a0..2623e80eae 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -4,6 +4,8 @@ #include #include "event_converter.h" +#include "aoa_hid.h" +#include "hid_keyboard.h" #include "util/log.h" static const int ACTION_DOWN = 1; @@ -59,6 +61,9 @@ 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->control = options->control; im->forward_key_repeat = options->forward_key_repeat; im->prefer_text = options->prefer_text; @@ -80,6 +85,8 @@ input_manager_init(struct input_manager *im, struct controller *controller, im->last_keycode = SDLK_UNKNOWN; im->last_mod = 0; im->key_repeat = 0; + + hid_keyboard_init(); } static void @@ -319,6 +326,10 @@ rotate_client_right(struct screen *screen) { static void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { + // We don't need this if HID over AoAv2 is used. + if (im->use_hid_over_aoa) { + return; + } if (is_shortcut_mod(im, SDL_GetModState())) { // A shortcut must never generate text events return; @@ -397,7 +408,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, } static void -input_manager_process_key(struct input_manager *im, +input_manager_process_key_inject(struct input_manager *im, const SDL_KeyboardEvent *event) { // control: indicates the state of the command-line option --no-control bool control = im->control; @@ -577,6 +588,36 @@ input_manager_process_key(struct input_manager *im, } } +static void +input_manager_process_key_hid(struct input_manager *im, + const SDL_KeyboardEvent *event) { + // Mirror-only mode. + if (!im->control) { + return; + } + // In USB HID protocol, key repeat is handle by host (Android in this case), + // so just ignore key repeat here. + if (event->repeat) { + return; + } + hid_keyboard_update_state(event); + const unsigned char *hid_event = hid_keyboard_get_hid_event(); + if (aoa_send_hid_event(im->usb_handle, hid_event, + HID_KEYBOARD_KEY_LENGTH) < 0) { + LOGW("Could not send HID event"); + } +} + +static void +input_manager_process_key(struct input_manager *im, + const SDL_KeyboardEvent *event) { + if (im->use_hid_over_aoa) { + input_manager_process_key_hid(im, event); + } else { + input_manager_process_key_inject(im, event); + } +} + static bool convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, struct control_msg *to) { diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 1dd7825f51..79c423c7f2 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -6,6 +6,7 @@ #include #include +#include #include "controller.h" #include "fps_counter.h" @@ -16,6 +17,9 @@ struct input_manager { struct controller *controller; struct screen *screen; + libusb_device_handle *usb_handle; + bool use_hid_over_aoa; + // SDL reports repeated events as a boolean, but Android expects the actual // number of repetitions. This variable keeps track of the count. unsigned repeat; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6a2857884c..7ec5439b29 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -13,10 +13,12 @@ # include #endif +#include "aoa_hid.h" #include "controller.h" #include "decoder.h" #include "events.h" #include "file_handler.h" +#include "hid_keyboard.h" #include "input_manager.h" #include "recorder.h" #include "screen.h" @@ -414,6 +416,64 @@ scrcpy(const struct scrcpy_options *options) { input_manager_init(&s->input_manager, &s->controller, &s->screen, options); + libusb_device_handle *usb_handle = NULL; + libusb_device *usb_device = NULL; + + if (options->use_hid_over_aoa) { + s->input_manager.use_hid_over_aoa = true; + libusb_init(NULL); + usb_device = aoa_find_usb_device(options->usb_vid, + options->usb_pid); + if (!usb_device) { + LOGW("USB device %04x:%04x not found", options->usb_vid, + options->usb_pid); + LOGW("Fallback to inject mode"); + s->input_manager.use_hid_over_aoa = false; + } + } + + if (s->input_manager.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 (s->input_manager.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; + } + + struct libusb_device_descriptor desc; + // Make sure usb_device is found. + if (s->input_manager.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, REPORT_DESC_SIZE, + 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; + } + + // Finally successfully set up HID over AoAv2. + if (s->input_manager.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); + ret = event_loop(s, options); LOGD("quit..."); @@ -422,6 +482,12 @@ scrcpy(const struct scrcpy_options *options) { 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) { + aoa_unregister_hid(usb_handle); + libusb_close(usb_handle); + libusb_unref_device(usb_device); + } // 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..1d1e3c9b00 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -80,8 +80,11 @@ struct scrcpy_options { uint16_t window_width; uint16_t window_height; uint32_t display_id; + uint16_t usb_vid; + uint16_t usb_pid; sc_tick display_buffer; sc_tick v4l2_buffer; + bool use_hid_over_aoa; bool show_touches; bool fullscreen; bool always_on_top; @@ -132,6 +135,7 @@ struct scrcpy_options { .display_id = 0, \ .display_buffer = 0, \ .v4l2_buffer = 0, \ + .use_hid_over_aoa = false, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \