Skip to content

Commit

Permalink
Add basic support to HID over AoAv2 for keyboard
Browse files Browse the repository at this point in the history
This provides a better input experience via
simulate physical keyboard, it converts
SDL_KeyboardEvent to proper HID events and send it
via HID over AoAv2.

This is a rewriting and bugfix of the origin code
from [@amosbird](https://github.com/amosbird).
  • Loading branch information
AlynxZhou committed Sep 10, 2021
1 parent b5e98db commit 62f8b19
Show file tree
Hide file tree
Showing 13 changed files with 656 additions and 9 deletions.
15 changes: 9 additions & 6 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 \
Expand All @@ -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 \
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion app/meson.build
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
src = [
'src/main.c',
'src/adb.c',
'src/aoa_hid.c',
'src/cli.c',
'src/clock.c',
'src/compat.c',
Expand All @@ -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',
Expand Down Expand Up @@ -54,6 +56,7 @@ if not get_option('crossbuild_windows')
dependency('libavformat'),
dependency('libavcodec'),
dependency('libavutil'),
dependency('libusb-1.0'),
dependency('sdl2'),
]

Expand All @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions app/scrcpy.1
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
142 changes: 142 additions & 0 deletions app/src/aoa_hid.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#include "util/log.h"
#include "aoa_hid.h"

// See <https://source.android.com/devices/accessories/aoa2#hid-support>.
#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 <https://source.android.com/devices/accessories/aoa2.html#hid-support>.
// 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 <https://source.android.com/devices/accessories/aoa2.html#hid-support>.
// 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 <https://source.android.com/devices/accessories/aoa2.html#hid-support>.
// 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 <https://source.android.com/devices/accessories/aoa2.html#hid-support>.
*/
// 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 <https://source.android.com/devices/accessories/aoa2.html#hid-support>.
// 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;
}
20 changes: 20 additions & 0 deletions app/src/aoa_hid.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef AOA_HID_H
#define AOA_HID_H

#include <stdint.h>

#include <libusb-1.0/libusb.h>

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
38 changes: 37 additions & 1 deletion app/src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")) {
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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)) {
Expand Down
Loading

0 comments on commit 62f8b19

Please sign in to comment.