diff --git a/README.md b/README.md index 67fdf364..44f3d740 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.6.1) +# scrcpy (v2.7) scrcpy @@ -37,6 +37,7 @@ Its features include: - [camera mirroring](doc/camera.md) (Android 12+) - [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID) + - [gamepad](doc/gamepad.md) support - [OTG mode](doc/otg.md) - and more… @@ -111,6 +112,13 @@ Here are just some common examples. scrcpy --otg ``` + - Control the device using gamepad controllers plugged into the computer: + + ```bash + scrcpy --gamepad=uhid + scrcpy -G # short version + ``` + ## User documentation The application provides a lot of features and configuration options. They are @@ -122,6 +130,7 @@ documented in the following pages: - [Control](doc/control.md) - [Keyboard](doc/keyboard.md) - [Mouse](doc/mouse.md) + - [Gamepad](doc/gamepad.md) - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index e0928cbd..db825ecc 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -26,6 +26,8 @@ _scrcpy() { -e --select-tcpip -f --fullscreen --force-adb-forward + -G + --gamepad= -h --help -K --keyboard= @@ -127,6 +129,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) return ;; + --gamepad) + COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) + return + ;; --orientation|--display-orientation) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0f06ba4b..fa0fa84f 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -33,8 +33,10 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' + '-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]' + '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' {-h,--help}'[Print the help]' - '-K[Use UHID keyboard (same as --keyboard=uhid)]' + '-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' @@ -44,7 +46,7 @@ arguments=( '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID mouse (same as --mouse=uhid)]' + '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse-bind=[Configure bindings of secondary clicks]' diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index ef92d4a5..89431542 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=7.0.1 +VERSION=7.0.2 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=bce9eeb0f17ef8982390b1f37711a61b4290dc8c2a0c1a37b5857e85bfb0e4ff +SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389 cd "$SOURCES_DIR" diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 0a42bc1f..c8b62746 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.5 +VERSION=2.30.7 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=be3ca88f8c362704627a0bc5406edb2cd6cc6ba463596d81ebb7c2f18763d3bf +SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5 cd "$SOURCES_DIR" diff --git a/app/meson.build b/app/meson.build index b0a6aadb..fc752e86 100644 --- a/app/meson.build +++ b/app/meson.build @@ -15,6 +15,7 @@ src = [ 'src/demuxer.c', 'src/device_msg.c', 'src/display.c', + 'src/events.c', 'src/icon.c', 'src/file_pusher.c', 'src/fps_counter.c', @@ -31,10 +32,12 @@ src = [ 'src/screen.c', 'src/server.c', 'src/version.c', + 'src/hid/hid_gamepad.c', 'src/hid/hid_keyboard.c', 'src/hid/hid_mouse.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', + 'src/uhid/gamepad_uhid.c', 'src/uhid/keyboard_uhid.c', 'src/uhid/mouse_uhid.c', 'src/uhid/uhid_output.c', @@ -93,6 +96,7 @@ usb_support = get_option('usb') if usb_support src += [ 'src/usb/aoa_hid.c', + 'src/usb/gamepad_aoa.c', 'src/usb/keyboard_aoa.c', 'src/usb/mouse_aoa.c', 'src/usb/scrcpy_otg.c', diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 9e0d90c2..6454c88e 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.6.1" + VALUE "ProductVersion", "2.7" END END BLOCK "VarFileInfo" diff --git a/app/scrcpy.1 b/app/scrcpy.1 index de2b8ac6..a256c40e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -29,7 +29,7 @@ Default is 128K (128000). .BI "\-\-audio\-buffer " ms Configure the audio buffering delay (in milliseconds). -Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). +Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches). Default is 50. @@ -175,13 +175,28 @@ Start in fullscreen. .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. +.TP +.B \-G +Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. + +.TP +.BI "\-\-gamepad " mode +Select how to send gamepad inputs to the device. + +Possible values are "disabled", "uhid" and "aoa": + + - "disabled" does not send gamepad inputs to the device. + - "uhid" simulates physical HID gamepads using the Linux HID kernel module on the device. + - "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB. + +Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR. .TP .B \-h, \-\-help Print this help. .TP .B \-K -Same as \fB\-\-keyboard=uhid\fR. +Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. .TP .BI "\-\-keyboard " mode @@ -200,7 +215,7 @@ For "uhid" and "aoa", the keyboard layout must be configured (once and for all) This option is only available when the HID keyboard is enabled (or a physical keyboard is connected). -Also see \fB\-\-mouse\fR. +Also see \fB\-\-mouse\fR and \fB\-\-gamepad\fR. .TP .B \-\-kill\-adb\-on\-close @@ -246,7 +261,7 @@ Default is 0 (unlimited). .TP .B \-M -Same as \fB\-\-mouse=uhid\fR. +Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set. .TP .BI "\-\-max\-fps " value @@ -267,7 +282,7 @@ In "uhid" and "aoa" modes, the computer mouse is captured to control the device LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. -Also see \fB\-\-keyboard\fR. +Also see \fB\-\-keyboard\fR and \fB\-\-gamepad\fR. .TP .BI "\-\-mouse\-bind " xxxx[:xxxx] @@ -369,7 +384,7 @@ If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable ke It may only work over USB. -See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. +See \fB\-\-keyboard\fR, \fB\-\-mouse\fR and \fB\-\-gamepad\fR. .TP .BI "\-p, \-\-port " port\fR[:\fIport\fR] @@ -379,7 +394,7 @@ Default is 27183:27199. .TP \fB\-\-pause\-on\-exit\fR[=\fImode\fR] -Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occured). +Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occurred). This is useful to prevent the terminal window from automatically closing, so that error messages can be read. diff --git a/app/src/android/keycodes.h b/app/src/android/keycodes.h index 60465a18..03ebb9c8 100644 --- a/app/src/android/keycodes.h +++ b/app/src/android/keycodes.h @@ -633,7 +633,7 @@ enum android_keycode { * Toggles between BS and CS digital satellite services. */ AKEYCODE_TV_SATELLITE_SERVICE = 240, /** Toggle Network key. - * Toggles selecting broacast services. */ + * Toggles selecting broadcast services. */ AKEYCODE_TV_NETWORK = 241, /** Antenna/Cable key. * Toggles broadcast input source between antenna and cable. */ diff --git a/app/src/audio_player.c b/app/src/audio_player.c index dac85bf9..274b6948 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -5,7 +5,7 @@ #include "util/log.h" -#define SC_AUDIO_PLAYER_NDEBUG // comment to debug +//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug /** * Real-time audio player with configurable latency @@ -72,7 +72,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { size_t len = len_int; uint32_t count = TO_SAMPLES(len); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif @@ -162,7 +162,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // swr_convert() returns the number of samples which would have been // written if the buffer was big enough. uint32_t samples = MIN(ret, dst_nb_samples); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); #endif @@ -244,7 +244,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, if (played) { LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 " samples", skip_samples); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG } else { LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", skip_samples); @@ -282,7 +282,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // However, the buffering level must be smoothed sc_average_push(&ap->avg_buffering, can_read); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", can_read, sc_average_get(&ap->avg_buffering)); #endif diff --git a/app/src/cli.c b/app/src/cli.c index dd1b6799..3c1f9a1b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -101,6 +101,7 @@ enum { OPT_MOUSE_BIND, OPT_NO_MOUSE_HOVER, OPT_AUDIO_DUP, + OPT_GAMEPAD, }; struct sc_option { @@ -156,7 +157,7 @@ static const struct sc_option options[] = { .argdesc = "ms", .text = "Configure the audio buffering delay (in milliseconds).\n" "Lower values decrease the latency, but increase the " - "likelyhood of buffer underrun (causing audio glitches).\n" + "likelihood of buffer underrun (causing audio glitches).\n" "Default is 50.", }, { @@ -372,6 +373,23 @@ static const struct sc_option options[] = { .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", }, + { + .shortopt = 'G', + .text = "Same as --gamepad=uhid, or --gamepad=aoa if --otg is set.", + }, + { + .longopt_id = OPT_GAMEPAD, + .longopt = "gamepad", + .argdesc = "mode", + .text = "Select how to send gamepad inputs to the device.\n" + "Possible values are \"disabled\", \"uhid\" and \"aoa\".\n" + "\"disabled\" does not send gamepad inputs to the device.\n" + "\"uhid\" simulates physical HID gamepads using the Linux UHID " + "kernel module on the device.\n" + "\"aoa\" simulates physical gamepads using the AOAv2 protocol." + "It may only work over USB.\n" + "Also see --keyboard and --mouse.", + }, { .shortopt = 'h', .longopt = "help", @@ -379,7 +397,7 @@ static const struct sc_option options[] = { }, { .shortopt = 'K', - .text = "Same as --keyboard=uhid.", + .text = "Same as --keyboard=uhid, or --keyboard=aoa if --otg is set.", }, { .longopt_id = OPT_KEYBOARD, @@ -403,7 +421,7 @@ static const struct sc_option options[] = { "start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n" "This option is only available when a HID keyboard is enabled " "(or a physical keyboard is connected).\n" - "Also see --mouse.", + "Also see --mouse and --gamepad.", }, { .longopt_id = OPT_KILL_ADB_ON_CLOSE, @@ -475,7 +493,7 @@ static const struct sc_option options[] = { }, { .shortopt = 'M', - .text = "Same as --mouse=uhid.", + .text = "Same as --mouse=uhid, or --mouse=aoa if --otg is set.", }, { .longopt_id = OPT_MAX_FPS, @@ -502,7 +520,7 @@ static const struct sc_option options[] = { "to control the device directly (relative mouse mode).\n" "LAlt, LSuper or RSuper toggle the capture mode, to give " "control of the mouse back to the computer.\n" - "Also see --keyboard.", + "Also see --keyboard and --gamepad.", }, { .longopt_id = OPT_MOUSE_BIND, @@ -637,7 +655,7 @@ static const struct sc_option options[] = { "Keyboard and mouse may be disabled separately using" "--keyboard=disabled and --mouse=disabled.\n" "It may only work over USB.\n" - "See --keyboard and --mouse.", + "See --keyboard, --mouse and --gamepad.", }, { .shortopt = 'p', @@ -654,7 +672,7 @@ static const struct sc_option options[] = { .optional_arg = true, .text = "Configure pause on exit. Possible values are \"true\" (always " "pause on exit), \"false\" (never pause on exit) and " - "\"if-error\" (pause only if an error occured).\n" + "\"if-error\" (pause only if an error occurred).\n" "This is useful to prevent the terminal window from " "automatically closing, so that error messages can be read.\n" "Default is \"false\".\n" @@ -1349,7 +1367,7 @@ print_exit_status(const struct sc_exit_status *status, unsigned cols) { return; } - assert(strlen(text) >= 9); // Contains at least the initial identation + assert(strlen(text) >= 9); // Contains at least the initial indentation // text + 9 to remove the initial indentation printf(" %3d %s\n", status->value, text + 9); @@ -1473,18 +1491,6 @@ parse_max_size(const char *s, uint16_t *max_size) { return true; } -static bool -parse_max_fps(const char *s, uint16_t *max_fps) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps"); - if (!ok) { - return false; - } - - *max_fps = (uint16_t) value; - return true; -} - static bool parse_buffering_time(const char *s, sc_tick *tick) { long value; @@ -2058,6 +2064,32 @@ parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) { return false; } +static bool +parse_gamepad(const char *optarg, enum sc_gamepad_input_mode *mode) { + if (!strcmp(optarg, "disabled")) { + *mode = SC_GAMEPAD_INPUT_MODE_DISABLED; + return true; + } + + if (!strcmp(optarg, "uhid")) { + *mode = SC_GAMEPAD_INPUT_MODE_UHID; + return true; + } + + if (!strcmp(optarg, "aoa")) { +#ifdef HAVE_USB + *mode = SC_GAMEPAD_INPUT_MODE_AOA; + return true; +#else + LOGE("--gamepad=aoa is disabled."); + return false; +#endif + } + + LOGE("Unsupported gamepad: %s (expected disabled or aoa)", optarg); + return false; +} + static bool parse_time_limit(const char *s, sc_tick *tick) { long value; @@ -2220,7 +2252,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], args->help = true; break; case 'K': - opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID; + opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA; break; case OPT_KEYBOARD: if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) { @@ -2232,9 +2264,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "--keyboard=uhid instead."); return false; case OPT_MAX_FPS: - if (!parse_max_fps(optarg, &opts->max_fps)) { - return false; - } + opts->max_fps = optarg; break; case 'm': if (!parse_max_size(optarg, &opts->max_size)) { @@ -2242,7 +2272,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case 'M': - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID_OR_AOA; break; case OPT_MOUSE: if (!parse_mouse(optarg, &opts->mouse_input_mode)) { @@ -2626,6 +2656,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_AUDIO_DUP: opts->audio_dup = true; break; + case 'G': + opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA; + break; + case OPT_GAMEPAD: + if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2743,7 +2781,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) { opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA : SC_KEYBOARD_INPUT_MODE_SDK; + } else if (opts->keyboard_input_mode + == SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA) { + opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA + : SC_KEYBOARD_INPUT_MODE_UHID; } + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { if (otg) { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; @@ -2753,14 +2796,21 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } else { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; } + } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID_OR_AOA) { + opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA + : SC_MOUSE_INPUT_MODE_UHID; } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK && !opts->video_playback) { LOGE("SDK mouse mode requires video playback. Try --mouse=uhid."); return false; } + if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA) { + opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA + : SC_GAMEPAD_INPUT_MODE_UHID; + } } - // If mouse bindings are not explictly set, configure default bindings + // If mouse bindings are not explicitly set, configure default bindings 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); @@ -2814,9 +2864,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + enum sc_gamepad_input_mode gmode = opts->gamepad_input_mode; + if (gmode != SC_GAMEPAD_INPUT_MODE_AOA + && gmode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + LOGE("In OTG mode, --gamepad only supports aoa or disabled."); + return false; + } + if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED - && mmode == SC_MOUSE_INPUT_MODE_DISABLED) { - LOGE("Could not disable both keyboard and mouse in OTG mode."); + && mmode == SC_MOUSE_INPUT_MODE_DISABLED + && gmode == SC_GAMEPAD_INPUT_MODE_DISABLED) { + LOGE("Cannot not disable all inputs in OTG mode."); return false; } } @@ -2857,18 +2915,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { - LOGE("Could not specify both --camera-id and --camera-facing"); + LOGE("Cannot specify both --camera-id and --camera-facing"); return false; } if (opts->camera_size) { if (opts->max_size) { - LOGE("Could not specify both --camera-size and -m/--max-size"); + LOGE("Cannot specify both --camera-size and -m/--max-size"); return false; } if (opts->camera_ar) { - LOGE("Could not specify both --camera-size and --camera-ar"); + LOGE("Cannot specify both --camera-size and --camera-ar"); return false; } } @@ -3009,19 +3067,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (!opts->control) { if (opts->turn_screen_off) { - LOGE("Could not request to turn screen off if control is disabled"); + LOGE("Cannot request to turn screen off if control is disabled"); return false; } if (opts->stay_awake) { - LOGE("Could not request to stay awake if control is disabled"); + LOGE("Cannot request to stay awake if control is disabled"); return false; } if (opts->show_touches) { - LOGE("Could not request to show touches if control is disabled"); + LOGE("Cannot request to show touches if control is disabled"); return false; } if (opts->power_off_on_close) { - LOGE("Could not request power off on close if control is disabled"); + LOGE("Cannot request power off on close if control is disabled"); return false; } } @@ -3046,7 +3104,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], // OTG mode is compatible with only very few options. // Only report obvious errors. if (opts->record_filename) { - LOGE("OTG mode: could not record"); + LOGE("OTG mode: cannot record"); return false; } if (opts->turn_screen_off) { @@ -3101,7 +3159,7 @@ sc_get_pause_on_exit(int argc, char *argv[]) { if (!strcmp(value, "if-error")) { return SC_PAUSE_ON_EXIT_IF_ERROR; } - // Set to false, inclusing when the value is invalid + // Set to false, including when the value is invalid return SC_PAUSE_ON_EXIT_FALSE; } } diff --git a/app/src/clock.c b/app/src/clock.c index 92989bfe..8a77e514 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -4,7 +4,7 @@ #include "util/log.h" -#define SC_CLOCK_NDEBUG // comment to debug +//#define SC_CLOCK_DEBUG // uncomment to debug #define SC_CLOCK_RANGE 32 @@ -21,10 +21,12 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { } sc_tick offset = system - stream; - clock->offset = ((clock->range - 1) * clock->offset + offset) - / clock->range; + unsigned clock_weight = clock->range - 1; + unsigned value_weight = SC_CLOCK_RANGE - clock->range + 1; + clock->offset = (clock->offset * clock_weight + offset * value_weight) + / SC_CLOCK_RANGE; -#ifndef SC_CLOCK_NDEBUG +#ifdef SC_CLOCK_DEBUG LOGD("Clock estimation: pts + %" PRItick, clock->offset); #endif } diff --git a/app/src/compat.h b/app/src/compat.h index fd610c02..1995d384 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -8,7 +8,7 @@ #include #include -#ifndef __WIN32 +#ifndef _WIN32 # define PRIu64_ PRIu64 # define SC_PRIsizet "zu" #else diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 9b0fab67..d599b62d 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -83,15 +83,34 @@ write_position(uint8_t *buf, const struct sc_position *position) { sc_write16be(&buf[10], position->screen_size.height); } -// write length (4 bytes) + string (non null-terminated) +// Write truncated string, and return the size static size_t -write_string(const char *utf8, size_t max_len, uint8_t *buf) { +write_string_payload(uint8_t *payload, const char *utf8, size_t max_len) { + if (!utf8) { + return 0; + } size_t len = sc_str_utf8_truncation_index(utf8, max_len); + memcpy(payload, utf8, len); + return len; +} + +// Write length (4 bytes) + string (non null-terminated) +static size_t +write_string(uint8_t *buf, const char *utf8, size_t max_len) { + size_t len = write_string_payload(buf + 4, utf8, max_len); sc_write32be(buf, len); - memcpy(&buf[4], utf8, len); return 4 + len; } +// Write length (1 byte) + string (non null-terminated) +static size_t +write_string_tiny(uint8_t *buf, const char *utf8, size_t max_len) { + assert(max_len <= 0xFF); + size_t len = write_string_payload(buf + 1, utf8, max_len); + buf[0] = len; + return 1 + len; +} + size_t sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { buf[0] = msg->type; @@ -103,9 +122,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { sc_write32be(&buf[10], msg->inject_keycode.metastate); return 14; case SC_CONTROL_MSG_TYPE_INJECT_TEXT: { - size_t len = - write_string(msg->inject_text.text, - SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); + size_t len = write_string(&buf[1], msg->inject_text.text, + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); return 1 + len; } case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: @@ -137,24 +155,34 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: sc_write64be(&buf[1], msg->set_clipboard.sequence); buf[9] = !!msg->set_clipboard.paste; - size_t len = write_string(msg->set_clipboard.text, - SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[10]); + size_t len = write_string(&buf[10], msg->set_clipboard.text, + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); return 10 + len; case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; case SC_CONTROL_MSG_TYPE_UHID_CREATE: sc_write16be(&buf[1], msg->uhid_create.id); - sc_write16be(&buf[3], msg->uhid_create.report_desc_size); - memcpy(&buf[5], msg->uhid_create.report_desc, - msg->uhid_create.report_desc_size); - return 5 + msg->uhid_create.report_desc_size; + + size_t index = 3; + index += write_string_tiny(&buf[index], msg->uhid_create.name, 127); + + sc_write16be(&buf[index], msg->uhid_create.report_desc_size); + index += 2; + + memcpy(&buf[index], msg->uhid_create.report_desc, + msg->uhid_create.report_desc_size); + index += msg->uhid_create.report_desc_size; + + return index; case SC_CONTROL_MSG_TYPE_UHID_INPUT: sc_write16be(&buf[1], msg->uhid_input.id); sc_write16be(&buf[3], msg->uhid_input.size); memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size); return 5 + msg->uhid_input.size; + case SC_CONTROL_MSG_TYPE_UHID_DESTROY: + sc_write16be(&buf[1], msg->uhid_destroy.id); + return 3; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -252,10 +280,15 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; - case SC_CONTROL_MSG_TYPE_UHID_CREATE: - LOG_CMSG("UHID create [%" PRIu16 "] report_desc_size=%" PRIu16, - msg->uhid_create.id, msg->uhid_create.report_desc_size); + case SC_CONTROL_MSG_TYPE_UHID_CREATE: { + // Quote only if name is not null + const char *name = msg->uhid_create.name; + const char *quote = name ? "\"" : ""; + LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s " + "report_desc_size=%" PRIu16, msg->uhid_create.id, + quote, name, quote, msg->uhid_create.report_desc_size); break; + } case SC_CONTROL_MSG_TYPE_UHID_INPUT: { char *hex = sc_str_to_hex_string(msg->uhid_input.data, msg->uhid_input.size); @@ -269,6 +302,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } break; } + case SC_CONTROL_MSG_TYPE_UHID_DESTROY: + LOG_CMSG("UHID destroy [%" PRIu16 "]", msg->uhid_destroy.id); + break; case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: LOG_CMSG("open hard keyboard settings"); break; @@ -278,6 +314,16 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } } +bool +sc_control_msg_is_droppable(const struct sc_control_msg *msg) { + // Cannot drop UHID_CREATE messages, because it would cause all further + // UHID_INPUT messages for this device to be invalid. + // Cannot drop UHID_DESTROY messages either, because a further UHID_CREATE + // with the same id may fail. + return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE + && msg->type != SC_CONTROL_MSG_TYPE_UHID_DESTROY; +} + void sc_control_msg_destroy(struct sc_control_msg *msg) { switch (msg->type) { diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 80714096..1ae8cae4 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -39,6 +39,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, + SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, }; @@ -97,6 +98,7 @@ struct sc_control_msg { } set_screen_power_mode; struct { uint16_t id; + const char *name; // pointer to static data uint16_t report_desc_size; const uint8_t *report_desc; // pointer to static data } uhid_create; @@ -105,6 +107,9 @@ struct sc_control_msg { uint16_t size; uint8_t data[SC_HID_MAX_SIZE]; } uhid_input; + struct { + uint16_t id; + } uhid_destroy; }; }; @@ -116,6 +121,11 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf); void sc_control_msg_log(const struct sc_control_msg *msg); +// Even when the buffer is "full", some messages must absolutely not be dropped +// to avoid inconsistencies. +bool +sc_control_msg_is_droppable(const struct sc_control_msg *msg); + void sc_control_msg_destroy(struct sc_control_msg *msg); diff --git a/app/src/controller.c b/app/src/controller.c index d50e1921..749de0a5 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -4,7 +4,8 @@ #include "util/log.h" -#define SC_CONTROL_MSG_QUEUE_MAX 64 +// Drop droppable events above this limit +#define SC_CONTROL_MSG_QUEUE_LIMIT 60 static void sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error, @@ -22,7 +23,9 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, void *cbs_userdata) { sc_vecdeque_init(&controller->queue); - bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); + // Add 4 to support 4 non-droppable events without re-allocation + bool ok = sc_vecdeque_reserve(&controller->queue, + SC_CONTROL_MSG_QUEUE_LIMIT + 4); if (!ok) { return false; } @@ -93,20 +96,31 @@ sc_controller_push_msg(struct sc_controller *controller, sc_control_msg_log(msg); } + bool pushed = false; + sc_mutex_lock(&controller->mutex); - bool full = sc_vecdeque_is_full(&controller->queue); - if (!full) { + size_t size = sc_vecdeque_size(&controller->queue); + if (size < SC_CONTROL_MSG_QUEUE_LIMIT) { bool was_empty = sc_vecdeque_is_empty(&controller->queue); sc_vecdeque_push_noresize(&controller->queue, *msg); + pushed = true; if (was_empty) { sc_cond_signal(&controller->msg_cond); } + } else if (!sc_control_msg_is_droppable(msg)) { + bool ok = sc_vecdeque_push(&controller->queue, *msg); + if (ok) { + pushed = true; + } else { + // A non-droppable event must be dropped anyway + LOG_OOM(); + } } - // Otherwise (if the queue is full), the msg is discarded + // Otherwise, the msg is discarded sc_mutex_unlock(&controller->mutex); - return !full; + return pushed; } static bool diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index f6141b35..e89a2092 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -8,8 +8,6 @@ #include "util/log.h" -#define SC_BUFFERING_NDEBUG // comment to debug - /** Downcast frame_sink to sc_delay_buffer */ #define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink) @@ -80,7 +78,7 @@ run_buffering(void *data) { goto stopped; } -#ifndef SC_BUFFERING_NDEBUG +#ifdef SC_BUFFERING_DEBUG LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, pts, dframe.push_date, sc_tick_now()); #endif @@ -134,6 +132,7 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink, sc_clock_init(&db->clock); sc_vecdeque_init(&db->queue); + db->stopped = false; if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) { goto error_destroy_wait_cond; @@ -206,7 +205,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, return false; } -#ifndef SC_BUFFERING_NDEBUG +#ifdef SC_BUFFERING_DEBUG dframe.push_date = sc_tick_now(); #endif diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 53592372..18c1ce94 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -12,12 +12,14 @@ #include "util/tick.h" #include "util/vecdeque.h" +//#define SC_BUFFERING_DEBUG // uncomment to debug + // forward declarations typedef struct AVFrame AVFrame; struct sc_delayed_frame { AVFrame *frame; -#ifndef NDEBUG +#ifdef SC_BUFFERING_DEBUG sc_tick push_date; #endif }; diff --git a/app/src/events.c b/app/src/events.c new file mode 100644 index 00000000..ce885241 --- /dev/null +++ b/app/src/events.c @@ -0,0 +1,66 @@ +#include "events.h" + +#include "util/log.h" +#include "util/thread.h" + +bool +sc_push_event_impl(uint32_t type, const char *name) { + SDL_Event event; + event.type = type; + int ret = SDL_PushEvent(&event); + // ret < 0: error (queue full) + // ret == 0: event was filtered + // ret == 1: success + if (ret != 1) { + LOGE("Could not post %s event: %s", name, SDL_GetError()); + return false; + } + + return true; +} + +bool +sc_post_to_main_thread(sc_runnable_fn run, void *userdata) { + SDL_Event event = { + .user = { + .type = SC_EVENT_RUN_ON_MAIN_THREAD, + .data1 = run, + .data2 = userdata, + }, + }; + int ret = SDL_PushEvent(&event); + // ret < 0: error (queue full) + // ret == 0: event was filtered + // ret == 1: success + if (ret != 1) { + if (ret == 0) { + // if ret == 0, this is expected on exit, log in debug mode + LOGD("Could not post runnable to main thread (filtered)"); + } else { + assert(ret < 0); + LOGW("Could not post runnable to main thread: %s", SDL_GetError()); + } + return false; + } + + return true; +} + +static int SDLCALL +task_event_filter(void *userdata, SDL_Event *event) { + (void) userdata; + + if (event->type == SC_EVENT_RUN_ON_MAIN_THREAD) { + // Reject this event type from now on + return 0; + } + + return 1; +} + +void +sc_reject_new_runnables(void) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + SDL_SetEventFilter(task_event_filter, NULL); +} diff --git a/app/src/events.h b/app/src/events.h index 3cf2b1dd..59c55de4 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,10 +1,38 @@ -#define SC_EVENT_NEW_FRAME SDL_USEREVENT -#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1) -#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) -#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) -#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) -#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) -#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) -#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7) -#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8) -#define SC_EVENT_CONTROLLER_ERROR (SDL_USEREVENT + 9) +#ifndef SC_EVENTS_H +#define SC_EVENTS_H + +#include "common.h" + +#include +#include +#include + +enum { + SC_EVENT_NEW_FRAME = SDL_USEREVENT, + SC_EVENT_RUN_ON_MAIN_THREAD, + SC_EVENT_DEVICE_DISCONNECTED, + SC_EVENT_SERVER_CONNECTION_FAILED, + SC_EVENT_SERVER_CONNECTED, + SC_EVENT_USB_DEVICE_DISCONNECTED, + SC_EVENT_DEMUXER_ERROR, + SC_EVENT_RECORDER_ERROR, + SC_EVENT_SCREEN_INIT_SIZE, + SC_EVENT_TIME_LIMIT_REACHED, + SC_EVENT_CONTROLLER_ERROR, + SC_EVENT_AOA_OPEN_ERROR, +}; + +bool +sc_push_event_impl(uint32_t type, const char *name); + +#define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE) + +typedef void (*sc_runnable_fn)(void *userdata); + +bool +sc_post_to_main_thread(sc_runnable_fn run, void *userdata); + +void +sc_reject_new_runnables(void); + +#endif diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index e17f8569..37c3611b 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -5,11 +5,23 @@ #include -#define SC_HID_MAX_SIZE 8 +#define SC_HID_MAX_SIZE 15 -struct sc_hid_event { +struct sc_hid_input { + uint16_t hid_id; uint8_t data[SC_HID_MAX_SIZE]; uint8_t size; }; +struct sc_hid_open { + uint16_t hid_id; + const char *name; // pointer to static memory + const uint8_t *report_desc; // pointer to static memory + size_t report_desc_size; +}; + +struct sc_hid_close { + uint16_t hid_id; +}; + #endif diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c new file mode 100644 index 00000000..e2bf0616 --- /dev/null +++ b/app/src/hid/hid_gamepad.c @@ -0,0 +1,457 @@ +#include "hid_gamepad.h" + +#include +#include + +#include "util/binary.h" +#include "util/log.h" + +// 2x2 bytes for left stick (X, Y) +// 2x2 bytes for right stick (Z, Rz) +// 2x2 bytes for L2/R2 triggers +// 2 bytes for buttons + padding, +// 1 byte for hat switch (dpad) + padding +#define SC_HID_GAMEPAD_EVENT_SIZE 15 + +// The ->buttons field stores the state for all buttons, but only some of them +// (the 16 LSB) must be transmitted "as is". The DPAD (hat switch) buttons are +// stored locally in the MSB of this field, but not transmitted as is: they are +// transformed to generate another specific byte. +#define SC_HID_BUTTONS_MASK 0xFFFF + +// outside SC_HID_BUTTONS_MASK +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_UP UINT32_C(0x10000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN UINT32_C(0x20000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT UINT32_C(0x40000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT UINT32_C(0x80000) + +/** + * Gamepad descriptor manually crafted to transmit the input reports. + * + * The HID specification is available here: + * + * + * The HID Usage Tables is also useful: + * + */ +static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Gamepad) + 0x09, 0x05, + + // Collection (Application) + 0xA1, 0x01, + + // Collection (Physical) + 0xA1, 0x00, + + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (X) Left stick x + 0x09, 0x30, + // Usage (Y) Left stick y + 0x09, 0x31, + // Usage (Z) Right stick x + 0x09, 0x32, + // Usage (Rz) Right stick y + 0x09, 0x35, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (65535) + // Cannot use 26 FF FF because 0xFFFF is interpreted as signed 16-bit + 0x27, 0xFF, 0xFF, 0x00, 0x00, // little-endian + // Report Size (16) + 0x75, 0x10, + // Report Count (4) + 0x95, 0x04, + // Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz) + 0x81, 0x02, + + // Usage Page (Simulation Controls) + 0x05, 0x02, + // Usage (Brake) + 0x09, 0xC5, + // Usage (Accelerator) + 0x09, 0xC4, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (32767) + 0x26, 0xFF, 0x7F, + // Report Size (16) + 0x75, 0x10, + // Report Count (2) + 0x95, 0x02, + // Input (Data, Variable, Absolute): 2 bytes (L2, R2) + 0x81, 0x02, + + // Usage Page (Buttons) + 0x05, 0x09, + // Usage Minimum (1) + 0x19, 0x01, + // Usage Maximum (16) + 0x29, 0x10, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Report Count (16) + 0x95, 0x10, + // Report Size (1) + 0x75, 0x01, + // Input (Data, Variable, Absolute): 16 buttons bits + 0x81, 0x02, + + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Hat switch) + 0x09, 0x39, + // Logical Minimum (1) + 0x15, 0x01, + // Logical Maximum (8) + 0x25, 0x08, + // Report Size (4) + 0x75, 0x04, + // Report Count (1) + 0x95, 0x01, + // Input (Data, Variable, Null State): 4-bit value + 0x81, 0x42, + + // End Collection + 0xC0, + + // End Collection + 0xC0, +}; + +/** + * A gamepad HID input report is 15 bytes long: + * - bytes 0-3: left stick state + * - bytes 4-7: right stick state + * - bytes 8-11: L2/R2 triggers state + * - bytes 12-13: buttons state + * - bytes 14: hat switch position (dpad) + * + * +---------------+ + * byte 0: |. . . . . . . .| + * | | left stick x (0-65535, little-endian) + * byte 1: |. . . . . . . .| + * +---------------+ + * byte 2: |. . . . . . . .| + * | | left stick y (0-65535, little-endian) + * byte 3: |. . . . . . . .| + * +---------------+ + * byte 4: |. . . . . . . .| + * | | right stick x (0-65535, little-endian) + * byte 5: |. . . . . . . .| + * +---------------+ + * byte 6: |. . . . . . . .| + * | | right stick y (0-65535, little-endian) + * byte 7: |. . . . . . . .| + * +---------------+ + * byte 8: |. . . . . . . .| + * | | L2 trigger (0-32767, little-endian) + * byte 9: |0 . . . . . . .| + * +---------------+ + * byte 10: |. . . . . . . .| + * | | R2 trigger (0-32767, little-endian) + * byte 11: |0 . . . . . . .| + * +---------------+ + * + * ,--------------- SC_GAMEPAD_BUTTON_RIGHT_SHOULDER + * | ,------------- SC_GAMEPAD_BUTTON_LEFT_SHOULDER + * | | + * | | ,--------- SC_GAMEPAD_BUTTON_NORTH + * | | | ,------- SC_GAMEPAD_BUTTON_WEST + * | | | | + * | | | | ,--- SC_GAMEPAD_BUTTON_EAST + * | | | | | ,- SC_GAMEPAD_BUTTON_SOUTH + * v v v v v v + * +---------------+ + * byte 12: |. . 0 . . 0 . .| + * | | Buttons (16-bit little-endian) + * byte 13: |0 . . . . . 0 0| + * +---------------+ + * ^ ^ ^ ^ ^ + * | | | | | + * | | | | | + * | | | | `----- SC_GAMEPAD_BUTTON_BACK + * | | | `------- SC_GAMEPAD_BUTTON_START + * | | `--------- SC_GAMEPAD_BUTTON_GUIDE + * | `----------- SC_GAMEPAD_BUTTON_LEFT_STICK + * `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK + * + * +---------------+ + * byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8) + * +---------------+ + * 9 possible positions and their values: + * 8 1 2 + * 7 0 3 + * 6 5 4 + * (8 is top-left, 1 is top, 2 is top-right, etc.) + */ + +static void +sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + slot->gamepad_id = gamepad_id; + slot->buttons = 0; + slot->axis_left_x = 0; + slot->axis_left_y = 0; + slot->axis_right_x = 0; + slot->axis_right_y = 0; + slot->axis_left_trigger = 0; + slot->axis_right_trigger = 0; +} + +static ssize_t +sc_hid_gamepad_slot_find(struct sc_hid_gamepad *hid, uint32_t gamepad_id) { + for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) { + if (gamepad_id == hid->slots[i].gamepad_id) { + // found + return i; + } + } + + return -1; +} + +void +sc_hid_gamepad_init(struct sc_hid_gamepad *hid) { + for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) { + hid->slots[i].gamepad_id = SC_GAMEPAD_ID_INVALID; + } +} + +static inline uint16_t +sc_hid_gamepad_slot_get_id(size_t slot_idx) { + assert(slot_idx < SC_MAX_GAMEPADS); + return SC_HID_ID_GAMEPAD_FIRST + slot_idx; +} + +bool +sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, + struct sc_hid_open *hid_open, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, SC_GAMEPAD_ID_INVALID); + if (slot_idx == -1) { + LOGW("No gamepad slot available for new gamepad %" PRIu32, gamepad_id); + return false; + } + + sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); + + SDL_GameController* game_controller = + SDL_GameControllerFromInstanceID(gamepad_id); + assert(game_controller); + const char *name = SDL_GameControllerName(game_controller); + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + hid_open->hid_id = hid_id; + hid_open->name = name; + hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); + + return true; +} + +bool +sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid, + struct sc_hid_close *hid_close, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Unknown gamepad removed %" PRIu32, gamepad_id); + return false; + } + + hid->slots[slot_idx].gamepad_id = SC_GAMEPAD_ID_INVALID; + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + hid_close->hid_id = hid_id; + + return true; +} + +static uint8_t +sc_hid_gamepad_get_dpad_value(uint32_t buttons) { + // Value depending on direction: + // 8 1 2 + // 7 0 3 + // 6 5 4 + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_UP) { + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 8; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 2; + } + return 1; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN) { + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 6; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 4; + } + return 5; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 7; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 3; + } + + return 0; +} + +static void +sc_hid_gamepad_event_from_slot(uint16_t hid_id, + const struct sc_hid_gamepad_slot *slot, + struct sc_hid_input *hid_input) { + hid_input->hid_id = hid_id; + hid_input->size = SC_HID_GAMEPAD_EVENT_SIZE; + + uint8_t *data = hid_input->data; + // Values must be written in little-endian + sc_write16le(data, slot->axis_left_x); + sc_write16le(data + 2, slot->axis_left_y); + sc_write16le(data + 4, slot->axis_right_x); + sc_write16le(data + 6, slot->axis_right_y); + sc_write16le(data + 8, slot->axis_left_trigger); + sc_write16le(data + 10, slot->axis_right_trigger); + sc_write16le(data + 12, slot->buttons & SC_HID_BUTTONS_MASK); + data[14] = sc_hid_gamepad_get_dpad_value(slot->buttons); +} + +static uint32_t +sc_hid_gamepad_get_button_id(enum sc_gamepad_button button) { + switch (button) { + case SC_GAMEPAD_BUTTON_SOUTH: + return 0x0001; + case SC_GAMEPAD_BUTTON_EAST: + return 0x0002; + case SC_GAMEPAD_BUTTON_WEST: + return 0x0008; + case SC_GAMEPAD_BUTTON_NORTH: + return 0x0010; + case SC_GAMEPAD_BUTTON_BACK: + return 0x0400; + case SC_GAMEPAD_BUTTON_GUIDE: + return 0x1000; + case SC_GAMEPAD_BUTTON_START: + return 0x0800; + case SC_GAMEPAD_BUTTON_LEFT_STICK: + return 0x2000; + case SC_GAMEPAD_BUTTON_RIGHT_STICK: + return 0x4000; + case SC_GAMEPAD_BUTTON_LEFT_SHOULDER: + return 0x0040; + case SC_GAMEPAD_BUTTON_RIGHT_SHOULDER: + return 0x0080; + case SC_GAMEPAD_BUTTON_DPAD_UP: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_UP; + case SC_GAMEPAD_BUTTON_DPAD_DOWN: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN; + case SC_GAMEPAD_BUTTON_DPAD_LEFT: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT; + case SC_GAMEPAD_BUTTON_DPAD_RIGHT: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT; + default: + // unknown button, ignore + return 0; + } +} + +bool +sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_button_event *event) { + if ((event->button < 0) || (event->button > 15)) { + return false; + } + + uint32_t gamepad_id = event->gamepad_id; + + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Axis event for unknown gamepad %" PRIu32, gamepad_id); + return false; + } + + assert(slot_idx < SC_MAX_GAMEPADS); + + struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; + + uint32_t button = sc_hid_gamepad_get_button_id(event->button); + if (!button) { + // unknown button, ignore + return false; + } + + if (event->action == SC_ACTION_DOWN) { + slot->buttons |= button; + } else { + assert(event->action == SC_ACTION_UP); + slot->buttons &= ~button; + } + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input); + + return true; +} + +bool +sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_axis_event *event) { + uint32_t gamepad_id = event->gamepad_id; + + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Button event for unknown gamepad %" PRIu32, gamepad_id); + return false; + } + + assert(slot_idx < SC_MAX_GAMEPADS); + + struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; + +// [-32768 to 32767] -> [0 to 65535] +#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) + switch (event->axis) { + case SC_GAMEPAD_AXIS_LEFTX: + slot->axis_left_x = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_LEFTY: + slot->axis_left_y = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_RIGHTX: + slot->axis_right_x = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_RIGHTY: + slot->axis_right_y = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_LEFT_TRIGGER: + // Trigger is always positive between 0 and 32767 + slot->axis_left_trigger = MAX(0, event->value); + break; + case SC_GAMEPAD_AXIS_RIGHT_TRIGGER: + // Trigger is always positive between 0 and 32767 + slot->axis_right_trigger = MAX(0, event->value); + break; + default: + return false; + } + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input); + + return true; +} diff --git a/app/src/hid/hid_gamepad.h b/app/src/hid/hid_gamepad.h new file mode 100644 index 00000000..b532a703 --- /dev/null +++ b/app/src/hid/hid_gamepad.h @@ -0,0 +1,53 @@ +#ifndef SC_HID_GAMEPAD_H +#define SC_HID_GAMEPAD_H + +#include "common.h" + +#include + +#include "hid/hid_event.h" +#include "input_events.h" + +#define SC_MAX_GAMEPADS 8 +#define SC_HID_ID_GAMEPAD_FIRST 3 +#define SC_HID_ID_GAMEPAD_LAST (SC_HID_ID_GAMEPAD_FIRST + SC_MAX_GAMEPADS - 1) + +struct sc_hid_gamepad_slot { + uint32_t gamepad_id; + uint32_t buttons; + uint16_t axis_left_x; + uint16_t axis_left_y; + uint16_t axis_right_x; + uint16_t axis_right_y; + uint16_t axis_left_trigger; + uint16_t axis_right_trigger; +}; + +struct sc_hid_gamepad { + struct sc_hid_gamepad_slot slots[SC_MAX_GAMEPADS]; +}; + +void +sc_hid_gamepad_init(struct sc_hid_gamepad *hid); + +bool +sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, + struct sc_hid_open *hid_open, + uint32_t gamepad_id); + +bool +sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid, + struct sc_hid_close *hid_close, + uint32_t gamepad_id); + +bool +sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_button_event *event); + +bool +sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_axis_event *event); + +#endif diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index f3001df4..2109224a 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -21,7 +21,7 @@ // keyboard support, though OS could support more keys via modifying the report // desc. 6 should be enough for scrcpy. #define SC_HID_KEYBOARD_MAX_KEYS 6 -#define SC_HID_KEYBOARD_EVENT_SIZE \ +#define SC_HID_KEYBOARD_INPUT_SIZE \ (SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS) #define SC_HID_RESERVED 0x00 @@ -31,13 +31,16 @@ * For HID, only report descriptor is needed. * * The specification is available here: - * + * * * In particular, read: - * - 6.2.2 Report Descriptor + * - §6.2.2 Report Descriptor * - Appendix B.1 Protocol 1 (Keyboard) * - Appendix C: Keyboard Implementation * + * The HID Usage Tables is also useful: + * + * * Normally a basic HID keyboard uses 8 bytes: * Modifier Reserved Key Key Key Key Key Key * @@ -47,7 +50,7 @@ * * (change vid:pid' to your device's vendor ID and product ID). */ -const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { +static const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Keyboard) @@ -60,7 +63,7 @@ const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { 0x05, 0x07, // Usage Minimum (224) 0x19, 0xE0, - // Usage Maximum (231) + // Usage Maximum (231) 0x29, 0xE7, // Logical Minimum (0) 0x15, 0x00, @@ -121,11 +124,8 @@ const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { 0xC0 }; -const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = - sizeof(SC_HID_KEYBOARD_REPORT_DESC); - /** - * A keyboard HID event is 8 bytes long: + * A keyboard HID input report is 8 bytes long: * * - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys) * - byte 1: reserved (always 0) @@ -199,10 +199,11 @@ const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = */ static void -sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { - hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE; +sc_hid_keyboard_input_init(struct sc_hid_input *hid_input) { + hid_input->hid_id = SC_HID_ID_KEYBOARD; + hid_input->size = SC_HID_KEYBOARD_INPUT_SIZE; - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE; data[1] = SC_HID_RESERVED; @@ -250,9 +251,9 @@ scancode_is_modifier(enum sc_scancode scancode) { } bool -sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, - struct sc_hid_event *hid_event, - const struct sc_key_event *event) { +sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_input *hid_input, + const struct sc_key_event *event) { enum sc_scancode scancode = event->scancode; assert(scancode >= 0); @@ -264,7 +265,7 @@ sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, return false; } - sc_hid_keyboard_event_init(hid_event); + sc_hid_keyboard_input_init(hid_input); uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state); @@ -275,9 +276,9 @@ sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, hid->keys[scancode] ? "true" : "false"); } - hid_event->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; + hid_input->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; - uint8_t *keys_data = &hid_event->data[SC_HID_KEYBOARD_INDEX_KEYS]; + uint8_t *keys_data = &hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS]; // Re-calculate pressed keys every time int keys_pressed_count = 0; for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { @@ -308,8 +309,8 @@ end: } bool -sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, - uint16_t mods_state) { +sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, + uint16_t mods_state) { bool capslock = mods_state & SC_MOD_CAPS; bool numlock = mods_state & SC_MOD_NUM; if (!capslock && !numlock) { @@ -317,17 +318,28 @@ sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, return false; } - sc_hid_keyboard_event_init(event); + sc_hid_keyboard_input_init(hid_input); unsigned i = 0; if (capslock) { - event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; + hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; ++i; } if (numlock) { - event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; + hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; ++i; } return true; } + +void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { + hid_open->hid_id = SC_HID_ID_KEYBOARD; + hid_open->name = NULL; // No name specified after "scrcpy" + hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); +} + +void sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close) { + hid_close->hid_id = SC_HID_ID_KEYBOARD; +} diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index ddd2cc91..cde1ac52 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -14,8 +14,7 @@ // 0x65 is Application, typically AT-101 Keyboard ends here. #define SC_HID_KEYBOARD_KEYS 0x66 -extern const uint8_t SC_HID_KEYBOARD_REPORT_DESC[]; -extern const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN; +#define SC_HID_ID_KEYBOARD 1 /** * HID keyboard events are sequence-based, every time keyboard state changes @@ -36,13 +35,19 @@ struct sc_hid_keyboard { void sc_hid_keyboard_init(struct sc_hid_keyboard *hid); -bool -sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, - struct sc_hid_event *hid_event, - const struct sc_key_event *event); +void +sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open); + +void +sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close); bool -sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, - uint16_t mods_state); +sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_input *hid_input, + const struct sc_key_event *event); + +bool +sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, + uint16_t mods_state); #endif diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 9d814448..ac215165 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -2,19 +2,19 @@ // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for wheel motion -#define HID_MOUSE_EVENT_SIZE 4 +#define SC_HID_MOUSE_INPUT_SIZE 4 /** * Mouse descriptor from the specification: - * + * * * Appendix E (p71): §E.10 Report Descriptor (Mouse) * * The usage tags (like Wheel) are listed in "HID Usage Tables": - * - * §4 Generic Desktop Page (0x01) (p26) + * + * §4 Generic Desktop Page (0x01) (p32) */ -const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { +static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Mouse) @@ -34,7 +34,7 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Minimum (1) 0x19, 0x01, - // Usage Maximum (5) + // Usage Maximum (5) 0x29, 0x05, // Logical Minimum (0) 0x15, 0x00, @@ -62,9 +62,9 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { 0x09, 0x31, // Usage (Wheel) 0x09, 0x38, - // Local Minimum (-127) + // Logical Minimum (-127) 0x15, 0x81, - // Local Maximum (127) + // Logical Maximum (127) 0x25, 0x7F, // Report Size (8) 0x75, 0x08, @@ -80,11 +80,8 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { 0xC0, }; -const size_t SC_HID_MOUSE_REPORT_DESC_LEN = - sizeof(SC_HID_MOUSE_REPORT_DESC); - /** - * A mouse HID event is 4 bytes long: + * A mouse HID input report is 4 bytes long: * * - byte 0: buttons state * - byte 1: relative x motion (signed byte from -127 to 127) @@ -125,10 +122,10 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = */ static void -sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { - hid_event->size = HID_MOUSE_EVENT_SIZE; - // Leave hid_event->data uninitialized, it will be fully initialized by - // callers +sc_hid_mouse_input_init(struct sc_hid_input *hid_input) { + hid_input->hid_id = SC_HID_ID_MOUSE; + hid_input->size = SC_HID_MOUSE_INPUT_SIZE; + // Leave ->data uninitialized, it will be fully initialized by callers } static uint8_t @@ -153,11 +150,11 @@ sc_hid_buttons_from_buttons_state(uint8_t buttons_state) { } void -sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, - const struct sc_mouse_motion_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, + const struct sc_mouse_motion_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = CLAMP(event->xrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127); @@ -165,11 +162,11 @@ sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, } void -sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, - const struct sc_mouse_click_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, + const struct sc_mouse_click_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = 0; // no x motion data[2] = 0; // no y motion @@ -177,11 +174,11 @@ sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, } void -sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, - const struct sc_mouse_scroll_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, + const struct sc_mouse_scroll_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = 0; // buttons state irrelevant (and unknown) data[1] = 0; // no x motion data[2] = 0; // no y motion @@ -190,3 +187,14 @@ sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, data[3] = CLAMP(event->vscroll, -127, 127); // Horizontal scrolling ignored } + +void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { + hid_open->hid_id = SC_HID_ID_MOUSE; + hid_open->name = NULL; // No name specified after "scrcpy" + hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); +} + +void sc_hid_mouse_generate_close(struct sc_hid_close *hid_close) { + hid_close->hid_id = SC_HID_ID_MOUSE; +} diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index e514d7d9..a9a54718 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -1,8 +1,6 @@ #ifndef SC_HID_MOUSE_H #define SC_HID_MOUSE_H -#endif - #include "common.h" #include @@ -10,17 +8,24 @@ #include "hid/hid_event.h" #include "input_events.h" -extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; -extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; +#define SC_HID_ID_MOUSE 2 void -sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, - const struct sc_mouse_motion_event *event); +sc_hid_mouse_generate_open(struct sc_hid_open *hid_open); void -sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, - const struct sc_mouse_click_event *event); +sc_hid_mouse_generate_close(struct sc_hid_close *hid_close); void -sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, - const struct sc_mouse_scroll_event *event); +sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, + const struct sc_mouse_motion_event *event); + +void +sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, + const struct sc_mouse_click_event *event); + +void +sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, + const struct sc_mouse_scroll_event *event); + +#endif diff --git a/app/src/input_events.h b/app/src/input_events.h index bbf4372f..c8966a35 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -323,6 +323,38 @@ enum sc_mouse_button { SC_MOUSE_BUTTON_X2 = SDL_BUTTON(SDL_BUTTON_X2), }; +// Use the naming from SDL3 for gamepad axis and buttons: +// + +enum sc_gamepad_axis { + SC_GAMEPAD_AXIS_UNKNOWN = -1, + SC_GAMEPAD_AXIS_LEFTX = SDL_CONTROLLER_AXIS_LEFTX, + SC_GAMEPAD_AXIS_LEFTY = SDL_CONTROLLER_AXIS_LEFTY, + SC_GAMEPAD_AXIS_RIGHTX = SDL_CONTROLLER_AXIS_RIGHTX, + SC_GAMEPAD_AXIS_RIGHTY = SDL_CONTROLLER_AXIS_RIGHTY, + SC_GAMEPAD_AXIS_LEFT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERLEFT, + SC_GAMEPAD_AXIS_RIGHT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERRIGHT, +}; + +enum sc_gamepad_button { + SC_GAMEPAD_BUTTON_UNKNOWN = -1, + SC_GAMEPAD_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A, + SC_GAMEPAD_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B, + SC_GAMEPAD_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X, + SC_GAMEPAD_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y, + SC_GAMEPAD_BUTTON_BACK = SDL_CONTROLLER_BUTTON_BACK, + SC_GAMEPAD_BUTTON_GUIDE = SDL_CONTROLLER_BUTTON_GUIDE, + SC_GAMEPAD_BUTTON_START = SDL_CONTROLLER_BUTTON_START, + SC_GAMEPAD_BUTTON_LEFT_STICK = SDL_CONTROLLER_BUTTON_LEFTSTICK, + SC_GAMEPAD_BUTTON_RIGHT_STICK = SDL_CONTROLLER_BUTTON_RIGHTSTICK, + SC_GAMEPAD_BUTTON_LEFT_SHOULDER = SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SC_GAMEPAD_BUTTON_RIGHT_SHOULDER = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, + SC_GAMEPAD_BUTTON_DPAD_UP = SDL_CONTROLLER_BUTTON_DPAD_UP, + SC_GAMEPAD_BUTTON_DPAD_DOWN = SDL_CONTROLLER_BUTTON_DPAD_DOWN, + SC_GAMEPAD_BUTTON_DPAD_LEFT = SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SC_GAMEPAD_BUTTON_DPAD_RIGHT = SDL_CONTROLLER_BUTTON_DPAD_RIGHT, +}; + static_assert(sizeof(enum sc_mod) >= sizeof(SDL_Keymod), "SDL_Keymod must be convertible to sc_mod"); @@ -380,6 +412,33 @@ struct sc_touch_event { float pressure; }; +enum sc_gamepad_device_event_type { + SC_GAMEPAD_DEVICE_ADDED, + SC_GAMEPAD_DEVICE_REMOVED, +}; + +// As documented in : +// The ID value starts at 0 and increments from there. The value -1 is an +// invalid ID. +#define SC_GAMEPAD_ID_INVALID UINT32_C(-1) + +struct sc_gamepad_device_event { + enum sc_gamepad_device_event_type type; + uint32_t gamepad_id; +}; + +struct sc_gamepad_button_event { + uint32_t gamepad_id; + enum sc_action action; + enum sc_gamepad_button button; +}; + +struct sc_gamepad_axis_event { + uint32_t gamepad_id; + enum sc_gamepad_axis axis; + int16_t value; +}; + static inline uint16_t sc_mods_state_from_sdl(uint16_t mods_state) { return mods_state; @@ -444,4 +503,43 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { return buttons_state; } +static inline enum sc_gamepad_device_event_type +sc_gamepad_device_event_type_from_sdl_type(uint32_t type) { + assert(type == SDL_CONTROLLERDEVICEADDED + || type == SDL_CONTROLLERDEVICEREMOVED); + if (type == SDL_CONTROLLERDEVICEADDED) { + return SC_GAMEPAD_DEVICE_ADDED; + } + return SC_GAMEPAD_DEVICE_REMOVED; +} + +static inline enum sc_gamepad_axis +sc_gamepad_axis_from_sdl(uint8_t axis) { + if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) { + // SC_GAMEPAD_AXIS_* constants are initialized from + // SDL_CONTROLLER_AXIS_* + return axis; + } + return SC_GAMEPAD_AXIS_UNKNOWN; +} + +static inline enum sc_gamepad_button +sc_gamepad_button_from_sdl(uint8_t button) { + if (button <= SDL_CONTROLLER_BUTTON_DPAD_RIGHT) { + // SC_GAMEPAD_BUTTON_* constants are initialized from + // SDL_CONTROLLER_BUTTON_* + return button; + } + return SC_GAMEPAD_BUTTON_UNKNOWN; +} + +static inline enum sc_action +sc_action_from_sdl_controllerbutton_type(uint32_t type) { + assert(type == SDL_CONTROLLERBUTTONDOWN || type == SDL_CONTROLLERBUTTONUP); + if (type == SDL_CONTROLLERBUTTONDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index d3c94d03..77cb4f1d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -56,16 +56,18 @@ void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { // A key/mouse processor may not be present if there is no controller - assert((!params->kp && !params->mp) || params->controller); + assert((!params->kp && !params->mp && !params->gp) || params->controller); // A processor must have ops initialized assert(!params->kp || params->kp->ops); assert(!params->mp || params->mp->ops); + assert(!params->gp || params->gp->ops); im->controller = params->controller; im->fp = params->fp; im->screen = params->screen; im->kp = params->kp; im->mp = params->mp; + im->gp = params->gp; im->mouse_bindings = params->mouse_bindings; im->legacy_paste = params->legacy_paste; @@ -400,7 +402,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, bool paused = im->screen->paused; bool video = im->screen->video; - SDL_Keycode keycode = event->keysym.sym; + SDL_Keycode sdl_keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; bool down = event->type == SDL_KEYDOWN; bool ctrl = event->keysym.mod & KMOD_CTRL; @@ -412,21 +414,21 @@ sc_input_manager_process_key(struct sc_input_manager *im, // The second condition is necessary to ignore the release of the modifier // key (because in this case mod is 0). bool is_shortcut = is_shortcut_mod(im, mod) - || is_shortcut_key(im, keycode); + || is_shortcut_key(im, sdl_keycode); if (down && !repeat) { - if (keycode == im->last_keycode && mod == im->last_mod) { + if (sdl_keycode == im->last_keycode && mod == im->last_mod) { ++im->key_repeat; } else { im->key_repeat = 0; - im->last_keycode = keycode; + im->last_keycode = sdl_keycode; im->last_mod = mod; } } if (is_shortcut) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - switch (keycode) { + switch (sdl_keycode) { case SDLK_h: if (im->kp && !shift && !repeat && !paused) { action_home(im, action); @@ -585,7 +587,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } uint64_t ack_to_wait = SC_SEQUENCE_INVALID; - bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat; + bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_v && down && !repeat; if (im->clipboard_autosync && is_ctrl_v) { if (im->legacy_paste) { // inject the text as input events @@ -613,10 +615,20 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } + enum sc_keycode keycode = sc_keycode_from_sdl(sdl_keycode); + if (keycode == SC_KEYCODE_UNKNOWN) { + return; + } + + enum sc_scancode scancode = sc_scancode_from_sdl(event->keysym.scancode); + if (scancode == SC_SCANCODE_UNKNOWN) { + return; + } + struct sc_key_event evt = { .action = sc_action_from_sdl_keyboard_type(event->type), - .keycode = sc_keycode_from_sdl(event->keysym.sym), - .scancode = sc_scancode_from_sdl(event->keysym.scancode), + .keycode = keycode, + .scancode = scancode, .repeat = event->repeat, .mods_state = sc_mods_state_from_sdl(event->keysym.mod), }; @@ -739,6 +751,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool down = event->type == SDL_MOUSEBUTTONDOWN; enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button); + if (button == SC_MOUSE_BUTTON_UNKNOWN) { + return; + } + if (!down) { // Mark the button as released im->mouse_buttons_state &= ~button; @@ -827,7 +843,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, 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), + .button = button, .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER : SC_POINTER_ID_MOUSE, .buttons_state = im->mouse_buttons_state, @@ -906,6 +922,78 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, im->mp->ops->process_mouse_scroll(im->mp, &evt); } +static void +sc_input_manager_process_gamepad_device(struct sc_input_manager *im, + const SDL_ControllerDeviceEvent *event) { + SDL_JoystickID id; + if (event->type == SDL_CONTROLLERDEVICEADDED) { + SDL_GameController *gc = SDL_GameControllerOpen(event->which); + if (!gc) { + LOGW("Could not open game controller"); + return; + } + + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc); + if (!joystick) { + LOGW("Could not get controller joystick"); + SDL_GameControllerClose(gc); + return; + } + + id = SDL_JoystickInstanceID(joystick); + } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + id = event->which; + + SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); + if (gc) { + SDL_GameControllerClose(gc); + } else { + LOGW("Unknown gamepad device removed"); + } + } else { + // Nothing to do + return; + } + + struct sc_gamepad_device_event evt = { + .type = sc_gamepad_device_event_type_from_sdl_type(event->type), + .gamepad_id = id, + }; + im->gp->ops->process_gamepad_device(im->gp, &evt); +} + +static void +sc_input_manager_process_gamepad_axis(struct sc_input_manager *im, + const SDL_ControllerAxisEvent *event) { + enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis); + if (axis == SC_GAMEPAD_AXIS_UNKNOWN) { + return; + } + + struct sc_gamepad_axis_event evt = { + .gamepad_id = event->which, + .axis = axis, + .value = event->value, + }; + im->gp->ops->process_gamepad_axis(im->gp, &evt); +} + +static void +sc_input_manager_process_gamepad_button(struct sc_input_manager *im, + const SDL_ControllerButtonEvent *event) { + enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button); + if (button == SC_GAMEPAD_BUTTON_UNKNOWN) { + return; + } + + struct sc_gamepad_button_event evt = { + .gamepad_id = event->which, + .action = sc_action_from_sdl_controllerbutton_type(event->type), + .button = button, + }; + im->gp->ops->process_gamepad_button(im->gp, &evt); +} + static bool is_apk(const char *file) { const char *ext = strrchr(file, '.'); @@ -978,6 +1066,27 @@ sc_input_manager_handle_event(struct sc_input_manager *im, } sc_input_manager_process_touch(im, &event->tfinger); break; + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + // Handle device added or removed even if paused + if (!im->gp) { + break; + } + sc_input_manager_process_gamepad_device(im, &event->cdevice); + break; + case SDL_CONTROLLERAXISMOTION: + if (!im->gp || paused) { + break; + } + sc_input_manager_process_gamepad_axis(im, &event->caxis); + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (!im->gp || paused) { + break; + } + sc_input_manager_process_gamepad_button(im, &event->cbutton); + break; case SDL_DROPFILE: { if (!control) { break; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 88558549..8efd0153 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -11,6 +11,7 @@ #include "file_pusher.h" #include "fps_counter.h" #include "options.h" +#include "trait/gamepad_processor.h" #include "trait/key_processor.h" #include "trait/mouse_processor.h" @@ -21,6 +22,7 @@ struct sc_input_manager { struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; @@ -50,6 +52,7 @@ struct sc_input_manager_params { struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; diff --git a/app/src/main.c b/app/src/main.c index 6050de11..8bbd074f 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -16,6 +16,7 @@ #include "usb/scrcpy_otg.h" #include "util/log.h" #include "util/net.h" +#include "util/thread.h" #include "version.h" #ifdef _WIN32 @@ -67,6 +68,9 @@ main_scrcpy(int argc, char *argv[]) { goto end; } + // The current thread is the main thread + SC_MAIN_THREAD_ID = sc_thread_get_id(); + #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL av_register_all(); #endif diff --git a/app/src/options.c b/app/src/options.c index 6fca6ad5..f8448792 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -23,6 +23,7 @@ const struct scrcpy_options scrcpy_options_default = { .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, + .gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED, .mouse_bindings = { .pri = { .right_click = SC_MOUSE_BINDING_AUTO, @@ -48,7 +49,7 @@ const struct scrcpy_options scrcpy_options_default = { .max_size = 0, .video_bit_rate = 0, .audio_bit_rate = 0, - .max_fps = 0, + .max_fps = NULL, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, diff --git a/app/src/options.h b/app/src/options.h index 140d12b1..5f6726e0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -142,6 +142,7 @@ enum sc_lock_video_orientation { enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_AUTO, + SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_KEYBOARD_INPUT_MODE_DISABLED, SC_KEYBOARD_INPUT_MODE_SDK, SC_KEYBOARD_INPUT_MODE_UHID, @@ -150,12 +151,20 @@ enum sc_keyboard_input_mode { enum sc_mouse_input_mode { SC_MOUSE_INPUT_MODE_AUTO, + SC_MOUSE_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_MOUSE_INPUT_MODE_DISABLED, SC_MOUSE_INPUT_MODE_SDK, SC_MOUSE_INPUT_MODE_UHID, SC_MOUSE_INPUT_MODE_AOA, }; +enum sc_gamepad_input_mode { + SC_GAMEPAD_INPUT_MODE_DISABLED, + SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode + SC_GAMEPAD_INPUT_MODE_UHID, + SC_GAMEPAD_INPUT_MODE_AOA, +}; + enum sc_mouse_binding { SC_MOUSE_BINDING_AUTO, SC_MOUSE_BINDING_DISABLED, @@ -231,6 +240,7 @@ struct scrcpy_options { enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; + enum sc_gamepad_input_mode gamepad_input_mode; struct sc_mouse_bindings mouse_bindings; enum sc_camera_facing camera_facing; struct sc_port_range port_range; @@ -240,7 +250,7 @@ struct scrcpy_options { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - uint16_t max_fps; + const char *max_fps; // float to be parsed by the server enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation display_orientation; enum sc_orientation record_orientation; diff --git a/app/src/receiver.c b/app/src/receiver.c index 3e572067..b89b0c6e 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -6,8 +6,17 @@ #include #include "device_msg.h" +#include "events.h" #include "util/log.h" #include "util/str.h" +#include "util/thread.h" + +struct sc_uhid_output_task_data { + struct sc_uhid_devices *uhid_devices; + uint16_t id; + uint16_t size; + uint8_t *data; +}; bool sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, @@ -33,20 +42,52 @@ sc_receiver_destroy(struct sc_receiver *receiver) { sc_mutex_destroy(&receiver->mutex); } +static void +task_set_clipboard(void *userdata) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + char *text = userdata; + + char *current = SDL_GetClipboardText(); + bool same = current && !strcmp(current, text); + SDL_free(current); + if (same) { + LOGD("Computer clipboard unchanged"); + } else { + LOGI("Device clipboard copied"); + SDL_SetClipboardText(text); + } + + free(text); +} + +static void +task_uhid_output(void *userdata) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + struct sc_uhid_output_task_data *data = userdata; + + sc_uhid_devices_process_hid_output(data->uhid_devices, data->id, data->data, + data->size); + + free(data->data); + free(data); +} + static void process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - char *current = SDL_GetClipboardText(); - bool same = current && !strcmp(current, msg->clipboard.text); - SDL_free(current); - if (same) { - LOGD("Computer clipboard unchanged"); + // Take ownership of the text (do not destroy the msg) + char *text = msg->clipboard.text; + + bool ok = sc_post_to_main_thread(task_set_clipboard, text); + if (!ok) { + LOGW("Could not post clipboard to main thread"); + free(text); return; } - LOGI("Device clipboard copied"); - SDL_SetClipboardText(msg->clipboard.text); break; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: @@ -64,6 +105,7 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { } sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); + // No allocation to free in the msg break; case DEVICE_MSG_TYPE_UHID_OUTPUT: if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { @@ -79,26 +121,35 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { } } - // This is a programming error to receive this message if there is - // no uhid_devices instance - assert(receiver->uhid_devices); - - // Also check at runtime (do not trust the server) if (!receiver->uhid_devices) { LOGE("Received unexpected HID output message"); + sc_device_msg_destroy(msg); return; } - struct sc_uhid_receiver *uhid_receiver = - sc_uhid_devices_get_receiver(receiver->uhid_devices, - msg->uhid_output.id); - if (uhid_receiver) { - uhid_receiver->ops->process_output(uhid_receiver, - msg->uhid_output.data, - msg->uhid_output.size); - } else { - LOGW("No UHID receiver for id %" PRIu16, msg->uhid_output.id); + struct sc_uhid_output_task_data *data = malloc(sizeof(*data)); + if (!data) { + LOG_OOM(); + return; } + + // It is guaranteed that these pointers will still be valid when + // the main thread will process them (the main thread will stop + // processing SC_EVENT_RUN_ON_MAIN_THREAD on exit, when everything + // gets deinitialized) + data->uhid_devices = receiver->uhid_devices; + data->id = msg->uhid_output.id; + data->data = msg->uhid_output.data; // take ownership + data->size = msg->uhid_output.size; + + bool ok = sc_post_to_main_thread(task_uhid_output, data); + if (!ok) { + LOGW("Could not post UHID output to main thread"); + free(data->data); + free(data); + return; + } + break; } } @@ -117,7 +168,7 @@ process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { } process_msg(receiver, &msg); - sc_device_msg_destroy(&msg); + // the device msg must be destroyed by process_msg() head += r; assert(head <= len); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 43864661..854657fb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -25,10 +25,12 @@ #include "recorder.h" #include "screen.h" #include "server.h" +#include "uhid/gamepad_uhid.h" #include "uhid/keyboard_uhid.h" #include "uhid/mouse_uhid.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" +# include "usb/gamepad_aoa.h" # include "usb/keyboard_aoa.h" # include "usb/mouse_aoa.h" # include "usb/usb.h" @@ -63,8 +65,8 @@ struct scrcpy { struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID struct sc_acksync acksync; - struct sc_uhid_devices uhid_devices; #endif + struct sc_uhid_devices uhid_devices; union { struct sc_keyboard_sdk keyboard_sdk; struct sc_keyboard_uhid keyboard_uhid; @@ -77,27 +79,21 @@ struct scrcpy { struct sc_mouse_uhid mouse_uhid; #ifdef HAVE_USB struct sc_mouse_aoa mouse_aoa; +#endif + }; + union { + struct sc_gamepad_uhid gamepad_uhid; +#ifdef HAVE_USB + struct sc_gamepad_aoa gamepad_aoa; #endif }; struct sc_timeout timeout; }; -static inline void -push_event(uint32_t type, const char *name) { - SDL_Event event; - event.type = type; - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGE("Could not post %s event: %s", name, SDL_GetError()); - // What could we do? - } -} -#define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE) - #ifdef _WIN32 static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { - PUSH_EVENT(SDL_QUIT); + sc_push_event(SDL_QUIT); return TRUE; } return FALSE; @@ -140,6 +136,10 @@ sdl_set_hints(const char *render_driver) { if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { LOGW("Could not disable minimize on focus loss"); } + + if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { + LOGW("Could not allow joystick background events"); + } } static void @@ -180,12 +180,21 @@ event_loop(struct scrcpy *s) { case SC_EVENT_RECORDER_ERROR: LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_AOA_OPEN_ERROR: + LOGE("AOA open error"); + return SCRCPY_EXIT_FAILURE; case SC_EVENT_TIME_LIMIT_REACHED: LOGI("Time limit reached"); return SCRCPY_EXIT_SUCCESS; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; + case SC_EVENT_RUN_ON_MAIN_THREAD: { + sc_runnable_fn run = event.user.data1; + void *userdata = event.user.data2; + run(userdata); + break; + } default: if (!sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; @@ -196,6 +205,21 @@ event_loop(struct scrcpy *s) { return SCRCPY_EXIT_FAILURE; } +static void +terminate_event_loop(void) { + sc_reject_new_runnables(); + + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SC_EVENT_RUN_ON_MAIN_THREAD) { + // Make sure all posted runnables are run, to avoid memory leaks + sc_runnable_fn run = event.user.data1; + void *userdata = event.user.data2; + run(userdata); + } + } +} + // Return true on success, false on error static bool await_for_server(bool *connected) { @@ -230,7 +254,7 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success, (void) userdata; if (!success) { - PUSH_EVENT(SC_EVENT_RECORDER_ERROR); + sc_push_event(SC_EVENT_RECORDER_ERROR); } } @@ -244,9 +268,9 @@ sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, assert(status != SC_DEMUXER_STATUS_DISABLED); if (status == SC_DEMUXER_STATUS_EOS) { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } else { - PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); + sc_push_event(SC_EVENT_DEMUXER_ERROR); } } @@ -260,11 +284,11 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, // Contrary to the video demuxer, keep mirroring if only the audio fails // (unless --require-audio is set). if (status == SC_DEMUXER_STATUS_EOS) { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } else if (status == SC_DEMUXER_STATUS_ERROR || (status == SC_DEMUXER_STATUS_DISABLED && options->require_audio)) { - PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); + sc_push_event(SC_EVENT_DEMUXER_ERROR); } } @@ -277,9 +301,9 @@ sc_controller_on_ended(struct sc_controller *controller, bool error, (void) userdata; if (error) { - PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); + sc_push_event(SC_EVENT_CONTROLLER_ERROR); } else { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } } @@ -288,7 +312,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; (void) userdata; - PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED); + sc_push_event(SC_EVENT_SERVER_CONNECTION_FAILED); } static void @@ -296,7 +320,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) { (void) server; (void) userdata; - PUSH_EVENT(SC_EVENT_SERVER_CONNECTED); + sc_push_event(SC_EVENT_SERVER_CONNECTED); } static void @@ -314,7 +338,7 @@ sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) { (void) timeout; (void) userdata; - PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED); + sc_push_event(SC_EVENT_TIME_LIMIT_REACHED); } // Generate a scrcpy id to differentiate multiple running scrcpy instances @@ -326,6 +350,21 @@ scrcpy_generate_scid(void) { return sc_rand_u32(&rand) & 0x7FFFFFFF; } +static void +init_sdl_gamepads(void) { + // Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already + // connected + int num_joysticks = SDL_NumJoysticks(); + for (int i = 0; i < num_joysticks; ++i) { + if (SDL_IsGameController(i)) { + SDL_Event event; + event.cdevice.type = SDL_CONTROLLERDEVICEADDED; + event.cdevice.which = i; + SDL_PushEvent(&event); + } + } +} + enum scrcpy_exit_code scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; @@ -358,6 +397,7 @@ scrcpy(struct scrcpy_options *options) { bool aoa_hid_initialized = false; bool keyboard_aoa_initialized = false; bool mouse_aoa_initialized = false; + bool gamepad_aoa_initialized = false; #endif bool controller_initialized = false; bool controller_started = false; @@ -366,7 +406,6 @@ scrcpy(struct scrcpy_options *options) { bool timeout_started = false; struct sc_acksync *acksync = NULL; - struct sc_uhid_devices *uhid_devices = NULL; uint32_t scid = scrcpy_generate_scid(); @@ -473,6 +512,13 @@ scrcpy(struct scrcpy_options *options) { } } + if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { + LOGE("Could not initialize SDL gamepad: %s", SDL_GetError()); + goto end; + } + } + sdl_configure(options->video_playback, options->disable_screensaver); // Await for server without blocking Ctrl+C handling @@ -570,6 +616,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_controller *controller = NULL; struct sc_key_processor *kp = NULL; struct sc_mouse_processor *mp = NULL; + struct sc_gamepad_processor *gp = NULL; if (options->control) { static const struct sc_controller_callbacks controller_cbs = { @@ -589,7 +636,9 @@ scrcpy(struct scrcpy_options *options) { options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; bool use_mouse_aoa = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; - if (use_keyboard_aoa || use_mouse_aoa) { + bool use_gamepad_aoa = + options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; + if (use_keyboard_aoa || use_mouse_aoa || use_gamepad_aoa) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; @@ -632,12 +681,15 @@ scrcpy(struct scrcpy_options *options) { goto end; } + bool aoa_fail = false; if (use_keyboard_aoa) { if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) { keyboard_aoa_initialized = true; kp = &s->keyboard_aoa.key_processor; } else { LOGE("Could not initialize HID keyboard"); + aoa_fail = true; + goto aoa_complete; } } @@ -647,12 +699,19 @@ scrcpy(struct scrcpy_options *options) { mp = &s->mouse_aoa.mouse_processor; } else { LOGE("Could not initialized HID mouse"); + aoa_fail = true; + goto aoa_complete; } } - bool need_aoa = keyboard_aoa_initialized || mouse_aoa_initialized; + if (use_gamepad_aoa) { + sc_gamepad_aoa_init(&s->gamepad_aoa, &s->aoa); + gp = &s->gamepad_aoa.gamepad_processor; + gamepad_aoa_initialized = true; + } - if (!need_aoa || !sc_aoa_start(&s->aoa)) { +aoa_complete: + if (aoa_fail || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); @@ -669,6 +728,8 @@ scrcpy(struct scrcpy_options *options) { assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); #endif + struct sc_keyboard_uhid *uhid_keyboard = NULL; + if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, options->key_inject_mode, @@ -676,14 +737,12 @@ scrcpy(struct scrcpy_options *options) { kp = &s->keyboard_sdk.key_processor; } else if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_UHID) { - sc_uhid_devices_init(&s->uhid_devices); - bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller, - &s->uhid_devices); + bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller); if (!ok) { goto end; } - uhid_devices = &s->uhid_devices; kp = &s->keyboard_uhid.key_processor; + uhid_keyboard = &s->keyboard_uhid; } if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { @@ -698,6 +757,17 @@ scrcpy(struct scrcpy_options *options) { mp = &s->mouse_uhid.mouse_processor; } + if (options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID) { + sc_gamepad_uhid_init(&s->gamepad_uhid, &s->controller); + gp = &s->gamepad_uhid.gamepad_processor; + } + + struct sc_uhid_devices *uhid_devices = NULL; + if (uhid_keyboard) { + sc_uhid_devices_init(&s->uhid_devices, uhid_keyboard); + uhid_devices = &s->uhid_devices; + } + sc_controller_configure(&s->controller, acksync, uhid_devices); if (!sc_controller_start(&s->controller)) { @@ -719,6 +789,7 @@ scrcpy(struct scrcpy_options *options) { .fp = fp, .kp = kp, .mp = mp, + .gp = gp, .mouse_bindings = options->mouse_bindings, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, @@ -830,7 +901,13 @@ scrcpy(struct scrcpy_options *options) { timeout_started = true; } + if (options->control + && options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + init_sdl_gamepads(); + } + ret = event_loop(s); + terminate_event_loop(); LOGD("quit..."); if (options->video_playback) { @@ -855,6 +932,9 @@ end: if (mouse_aoa_initialized) { sc_mouse_aoa_destroy(&s->mouse_aoa); } + if (gamepad_aoa_initialized) { + sc_gamepad_aoa_destroy(&s->gamepad_aoa); + } sc_aoa_stop(&s->aoa); sc_usb_stop(&s->usb); } diff --git a/app/src/screen.c b/app/src/screen.c index 55a06ab3..cb455cb1 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -299,6 +299,12 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink, struct sc_screen *screen = DOWNCAST(sink); + if (ctx->width <= 0 || ctx->width > 0xFFFF + || ctx->height <= 0 || ctx->height > 0xFFFF) { + LOGE("Invalid video size: %dx%d", ctx->width, ctx->height); + return false; + } + assert(ctx->width > 0 && ctx->width <= 0xFFFF); assert(ctx->height > 0 && ctx->height <= 0xFFFF); // screen->frame_size is never used before the event is pushed, and the @@ -306,14 +312,9 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink, screen->frame_size.width = ctx->width; screen->frame_size.height = ctx->height; - static SDL_Event event = { - .type = SC_EVENT_SCREEN_INIT_SIZE, - }; - // Post the event on the UI thread (the texture must be created from there) - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGW("Could not post init size event: %s", SDL_GetError()); + bool ok = sc_push_event(SC_EVENT_SCREEN_INIT_SIZE); + if (!ok) { return false; } @@ -352,14 +353,9 @@ sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { // The SC_EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead } else { - static SDL_Event new_frame_event = { - .type = SC_EVENT_NEW_FRAME, - }; - // Post the event on the UI thread - int ret = SDL_PushEvent(&new_frame_event); - if (ret < 0) { - LOGW("Could not post new frame event: %s", SDL_GetError()); + bool ok = sc_push_event(SC_EVENT_NEW_FRAME); + if (!ok) { return false; } } @@ -481,6 +477,7 @@ sc_screen_init(struct sc_screen *screen, .screen = screen, .kp = params->kp, .mp = params->mp, + .gp = params->gp, .mouse_bindings = params->mouse_bindings, .legacy_paste = params->legacy_paste, .clipboard_autosync = params->clipboard_autosync, diff --git a/app/src/screen.h b/app/src/screen.h index 079d4fbb..7e1f7e6e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -78,6 +78,7 @@ struct sc_screen_params { struct sc_file_pusher *fp; struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; diff --git a/app/src/server.c b/app/src/server.c index 41517f18..90a0ac5d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -218,6 +218,21 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { } } +static bool +validate_string(const char *s) { + // The parameters values are passed as command line arguments to adb, so + // they must either be properly escaped, or they must not contain any + // special shell characters. + // Since they are not properly escaped on Windows anyway (see + // sys/win/process.c), just forbid special shell characters. + if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~\r\n")) { + LOGE("Invalid server param: [%s]", s); + return false; + } + + return true; +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -260,6 +275,11 @@ execute_server(struct sc_server *server, } \ cmd[count++] = p; \ } while(0) +#define VALIDATE_STRING(s) do { \ + if (!validate_string(s)) { \ + goto end; \ + } \ + } while(0) ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); @@ -301,7 +321,8 @@ execute_server(struct sc_server *server, ADD_PARAM("max_size=%" PRIu16, params->max_size); } if (params->max_fps) { - ADD_PARAM("max_fps=%" PRIu16, params->max_fps); + VALIDATE_STRING(params->max_fps); + ADD_PARAM("max_fps=%s", params->max_fps); } if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { ADD_PARAM("lock_video_orientation=%" PRIi8, @@ -311,6 +332,7 @@ execute_server(struct sc_server *server, ADD_PARAM("tunnel_forward=true"); } if (params->crop) { + VALIDATE_STRING(params->crop); ADD_PARAM("crop=%s", params->crop); } if (!params->control) { @@ -321,9 +343,11 @@ execute_server(struct sc_server *server, ADD_PARAM("display_id=%" PRIu32, params->display_id); } if (params->camera_id) { + VALIDATE_STRING(params->camera_id); ADD_PARAM("camera_id=%s", params->camera_id); } if (params->camera_size) { + VALIDATE_STRING(params->camera_size); ADD_PARAM("camera_size=%s", params->camera_size); } if (params->camera_facing != SC_CAMERA_FACING_ANY) { @@ -331,6 +355,7 @@ execute_server(struct sc_server *server, sc_server_get_camera_facing_name(params->camera_facing)); } if (params->camera_ar) { + VALIDATE_STRING(params->camera_ar); ADD_PARAM("camera_ar=%s", params->camera_ar); } if (params->camera_fps) { @@ -346,15 +371,19 @@ execute_server(struct sc_server *server, ADD_PARAM("stay_awake=true"); } if (params->video_codec_options) { + VALIDATE_STRING(params->video_codec_options); ADD_PARAM("video_codec_options=%s", params->video_codec_options); } if (params->audio_codec_options) { + VALIDATE_STRING(params->audio_codec_options); ADD_PARAM("audio_codec_options=%s", params->audio_codec_options); } if (params->video_encoder) { + VALIDATE_STRING(params->video_encoder); ADD_PARAM("video_encoder=%s", params->video_encoder); } if (params->audio_encoder) { + VALIDATE_STRING(params->audio_encoder); ADD_PARAM("audio_encoder=%s", params->audio_encoder); } if (params->power_off_on_close) { @@ -630,6 +659,14 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { } } + if (control_socket != SC_SOCKET_NONE) { + // Disable Nagle's algorithm for the control socket + // (it only impacts the sending side, so it is useless to set it + // for the other sockets) + bool ok = net_set_tcp_nodelay(control_socket, true); + (void) ok; // error already logged + } + // we don't need the adb tunnel anymore sc_adb_tunnel_close(tunnel, &server->intr, serial, server->device_socket_name); diff --git a/app/src/server.h b/app/src/server.h index cffa510e..d9d42582 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,7 +44,7 @@ struct sc_server_params { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - uint16_t max_fps; + const char *max_fps; // float to be parsed by the server int8_t lock_video_orientation; bool control; uint32_t display_id; diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h new file mode 100644 index 00000000..72479783 --- /dev/null +++ b/app/src/trait/gamepad_processor.h @@ -0,0 +1,50 @@ +#ifndef SC_GAMEPAD_PROCESSOR_H +#define SC_GAMEPAD_PROCESSOR_H + +#include "common.h" + +#include +#include + +#include "input_events.h" + +/** + * Gamepad processor trait. + * + * Component able to handle gamepads devices and inject buttons and axis events. + */ +struct sc_gamepad_processor { + const struct sc_gamepad_processor_ops *ops; +}; + +struct sc_gamepad_processor_ops { + + /** + * Process a gamepad device added or removed + * + * This function is mandatory. + */ + void + (*process_gamepad_device)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event); + + /** + * Process a gamepad axis event + * + * This function is mandatory. + */ + void + (*process_gamepad_axis)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event); + + /** + * Process a gamepad button event + * + * This function is mandatory. + */ + void + (*process_gamepad_button)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_button_event *event); +}; + +#endif diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c new file mode 100644 index 00000000..62b0f653 --- /dev/null +++ b/app/src/uhid/gamepad_uhid.c @@ -0,0 +1,123 @@ +#include "gamepad_uhid.h" + +#include "hid/hid_gamepad.h" +#include "input_events.h" +#include "util/log.h" + +/** Downcast gamepad processor to sc_gamepad_uhid */ +#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) + +static void +sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_input *hid_input, + const char *name) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; + msg.uhid_input.id = hid_input->hid_id; + + assert(hid_input->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); + msg.uhid_input.size = hid_input->size; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_INPUT message (%s)", name); + } +} + +static void +sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_open *hid_open) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; + msg.uhid_create.id = hid_open->hid_id; + msg.uhid_create.name = hid_open->name; + msg.uhid_create.report_desc = hid_open->report_desc; + msg.uhid_create.report_desc_size = hid_open->report_desc_size; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_CREATE message (gamepad)"); + } +} + +static void +sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_close *hid_close) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_DESTROY; + msg.uhid_create.id = hid_close->hid_id; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_DESTROY message (gamepad)"); + } +} + +static void +sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + if (event->type == SC_GAMEPAD_DEVICE_ADDED) { + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; + } + + sc_gamepad_uhid_send_open(gamepad, &hid_open); + } else { + assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); + + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + sc_gamepad_uhid_send_close(gamepad, &hid_close); + } +} + +static void +sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, + event)) { + return; + } + + sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad axis"); +} + +static void +sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp, + const struct sc_gamepad_button_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, + event)) { + return; + } + + sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad button"); + +} + +void +sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad, + struct sc_controller *controller) { + sc_hid_gamepad_init(&gamepad->hid); + + gamepad->controller = controller; + + static const struct sc_gamepad_processor_ops ops = { + .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, + .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, + }; + + gamepad->gamepad_processor.ops = &ops; +} diff --git a/app/src/uhid/gamepad_uhid.h b/app/src/uhid/gamepad_uhid.h new file mode 100644 index 00000000..07d03099 --- /dev/null +++ b/app/src/uhid/gamepad_uhid.h @@ -0,0 +1,23 @@ +#ifndef SC_GAMEPAD_UHID_H +#define SC_GAMEPAD_UHID_H + +#include "common.h" + +#include + +#include "controller.h" +#include "hid/hid_gamepad.h" +#include "trait/gamepad_processor.h" + +struct sc_gamepad_uhid { + struct sc_gamepad_processor gamepad_processor; // gamepad processor trait + + struct sc_hid_gamepad hid; + struct sc_controller *controller; +}; + +void +sc_gamepad_uhid_init(struct sc_gamepad_uhid *mouse, + struct sc_controller *controller); + +#endif diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 515a3fd9..496da23d 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -9,21 +9,19 @@ #define DOWNCAST_RECEIVER(UR) \ container_of(UR, struct sc_keyboard_uhid, uhid_receiver) -#define UHID_KEYBOARD_ID 1 - static void sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, - const struct sc_hid_event *event) { + const struct sc_hid_input *hid_input) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = UHID_KEYBOARD_ID; + msg.uhid_input.id = hid_input->hid_id; - assert(event->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, event->data, event->size); - msg.uhid_input.size = event->size; + assert(hid_input->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); + msg.uhid_input.size = hid_input->size; if (!sc_controller_push_msg(kb->controller, &msg)) { - LOGE("Could not send UHID_INPUT message (key)"); + LOGE("Could not push UHID_INPUT message (key)"); } } @@ -31,23 +29,22 @@ static void sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { SDL_Keymod sdl_mod = SDL_GetModState(); uint16_t mod = sc_mods_state_from_sdl(sdl_mod) & (SC_MOD_CAPS | SC_MOD_NUM); - - uint16_t device_mod = - atomic_load_explicit(&kb->device_mod, memory_order_relaxed); - uint16_t diff = mod ^ device_mod; + uint16_t diff = mod ^ kb->device_mod; if (diff) { // Inherently racy (the HID output reports arrive asynchronously in // response to key presses), but will re-synchronize on next key press // or HID output anyway - atomic_store_explicit(&kb->device_mod, mod, memory_order_relaxed); + kb->device_mod = mod; - struct sc_hid_event hid_event; - sc_hid_keyboard_event_from_mods(&hid_event, diff); + struct sc_hid_input hid_input; + if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, diff)) { + return; + } LOGV("HID keyboard state synchronized"); - sc_keyboard_uhid_send_input(kb, &hid_event); + sc_keyboard_uhid_send_input(kb, &hid_input); } } @@ -57,6 +54,8 @@ sc_key_processor_process_key(struct sc_key_processor *kp, uint64_t ack_to_wait) { (void) ack_to_wait; + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + if (event->repeat) { // In USB HID protocol, key repeat is handled by the host (Android), so // just ignore key repeat here. @@ -65,22 +64,20 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct sc_keyboard_uhid *kb = DOWNCAST(kp); - struct sc_hid_event hid_event; + struct sc_hid_input hid_input; // Not all keys are supported, just ignore unsupported keys - if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) { if (event->scancode == SC_SCANCODE_CAPSLOCK) { - atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_CAPS, - memory_order_relaxed); + kb->device_mod ^= SC_MOD_CAPS; } else if (event->scancode == SC_SCANCODE_NUMLOCK) { - atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_NUM, - memory_order_relaxed); + kb->device_mod ^= SC_MOD_NUM; } else { // Synchronize modifiers (only if the scancode itself does not // change the modifiers) sc_keyboard_uhid_synchronize_mod(kb); } - sc_keyboard_uhid_send_input(kb, &hid_event); + sc_keyboard_uhid_send_input(kb, &hid_input); } } @@ -98,34 +95,31 @@ sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) { return mod; } -static void -sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, - const uint8_t *data, size_t len) { - // Called from the thread receiving device messages +void +sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb, + const uint8_t *data, size_t size) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); - assert(len); + assert(size); // Also check at runtime (do not trust the server) - if (!len) { + if (!size) { LOGE("Unexpected empty HID output message"); return; } - struct sc_keyboard_uhid *kb = DOWNCAST_RECEIVER(receiver); - uint8_t hid_led = data[0]; uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led); - atomic_store_explicit(&kb->device_mod, device_mod, memory_order_relaxed); + kb->device_mod = device_mod; } bool sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller, - struct sc_uhid_devices *uhid_devices) { + struct sc_controller *controller) { sc_hid_keyboard_init(&kb->hid); kb->controller = controller; - atomic_init(&kb->device_mod, 0); + kb->device_mod = 0; static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, @@ -140,19 +134,16 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, kb->key_processor.hid = true; kb->key_processor.ops = &ops; - static const struct sc_uhid_receiver_ops uhid_receiver_ops = { - .process_output = sc_uhid_receiver_process_output, - }; - - kb->uhid_receiver.id = UHID_KEYBOARD_ID; - kb->uhid_receiver.ops = &uhid_receiver_ops; - sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); + struct sc_hid_open hid_open; + sc_hid_keyboard_generate_open(&hid_open); + assert(hid_open.hid_id == SC_HID_ID_KEYBOARD); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = UHID_KEYBOARD_ID; - msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC; - msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN; + msg.uhid_create.id = SC_HID_ID_KEYBOARD; + msg.uhid_create.name = hid_open.name; + msg.uhid_create.report_desc = hid_open.report_desc; + msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { LOGE("Could not send UHID_CREATE message (keyboard)"); return false; diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h index 5e1be70c..1628a678 100644 --- a/app/src/uhid/keyboard_uhid.h +++ b/app/src/uhid/keyboard_uhid.h @@ -7,21 +7,22 @@ #include "controller.h" #include "hid/hid_keyboard.h" -#include "uhid/uhid_output.h" #include "trait/key_processor.h" struct sc_keyboard_uhid { struct sc_key_processor key_processor; // key processor trait - struct sc_uhid_receiver uhid_receiver; struct sc_hid_keyboard hid; struct sc_controller *controller; - atomic_uint_least16_t device_mod; + uint16_t device_mod; }; bool sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller, - struct sc_uhid_devices *uhid_devices); + struct sc_controller *controller); + +void +sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb, + const uint8_t *data, size_t size); #endif diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 77446f9e..1dc02777 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -7,21 +7,20 @@ /** Downcast mouse processor to mouse_uhid */ #define DOWNCAST(MP) container_of(MP, struct sc_mouse_uhid, mouse_processor) -#define UHID_MOUSE_ID 2 - static void sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, - const struct sc_hid_event *event, const char *name) { + const struct sc_hid_input *hid_input, + const char *name) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = UHID_MOUSE_ID; + msg.uhid_input.id = hid_input->hid_id; - assert(event->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, event->data, event->size); - msg.uhid_input.size = event->size; + assert(hid_input->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); + msg.uhid_input.size = hid_input->size; if (!sc_controller_push_msg(mouse->controller, &msg)) { - LOGE("Could not send UHID_INPUT message (%s)", name); + LOGE("Could not push UHID_INPUT message (%s)", name); } } @@ -30,10 +29,10 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_motion(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_motion(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse motion"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse motion"); } static void @@ -41,10 +40,10 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_click(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_click(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse click"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse click"); } static void @@ -52,10 +51,10 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_scroll(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse scroll"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); } bool @@ -75,13 +74,18 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, mouse->mouse_processor.relative_mode = true; + struct sc_hid_open hid_open; + sc_hid_mouse_generate_open(&hid_open); + assert(hid_open.hid_id == SC_HID_ID_MOUSE); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = UHID_MOUSE_ID; - msg.uhid_create.report_desc = SC_HID_MOUSE_REPORT_DESC; - msg.uhid_create.report_desc_size = SC_HID_MOUSE_REPORT_DESC_LEN; + msg.uhid_create.id = SC_HID_ID_MOUSE; + msg.uhid_create.name = hid_open.name; + msg.uhid_create.report_desc = hid_open.report_desc; + msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { - LOGE("Could not send UHID_CREATE message (mouse)"); + LOGE("Could not push UHID_CREATE message (mouse)"); return false; } diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c index 3b095faf..05e691da 100644 --- a/app/src/uhid/uhid_output.c +++ b/app/src/uhid/uhid_output.c @@ -1,25 +1,27 @@ #include "uhid_output.h" #include +#include + +#include "uhid/keyboard_uhid.h" +#include "util/log.h" void -sc_uhid_devices_init(struct sc_uhid_devices *devices) { - devices->count = 0; +sc_uhid_devices_init(struct sc_uhid_devices *devices, + struct sc_keyboard_uhid *keyboard) { + devices->keyboard = keyboard; } void -sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, - struct sc_uhid_receiver *receiver) { - assert(devices->count < SC_UHID_MAX_RECEIVERS); - devices->receivers[devices->count++] = receiver; -} - -struct sc_uhid_receiver * -sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id) { - for (size_t i = 0; i < devices->count; ++i) { - if (devices->receivers[i]->id == id) { - return devices->receivers[i]; +sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id, + const uint8_t *data, size_t size) { + if (id == SC_HID_ID_KEYBOARD) { + if (devices->keyboard) { + sc_keyboard_uhid_process_hid_output(devices->keyboard, data, size); + } else { + LOGW("Unexpected keyboard HID output without UHID keyboard"); } + } else { + LOGW("HID output ignored for id %" PRIu16, id); } - return NULL; } diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h index e13eed87..cd6a800f 100644 --- a/app/src/uhid/uhid_output.h +++ b/app/src/uhid/uhid_output.h @@ -9,37 +9,19 @@ /** * The communication with UHID devices is bidirectional. * - * This component manages the registration of receivers to handle UHID output - * messages (sent from the device to the computer). + * This component dispatches HID outputs to the expected processor. */ -struct sc_uhid_receiver { - uint16_t id; - - const struct sc_uhid_receiver_ops *ops; -}; - -struct sc_uhid_receiver_ops { - void - (*process_output)(struct sc_uhid_receiver *receiver, - const uint8_t *data, size_t len); -}; - -#define SC_UHID_MAX_RECEIVERS 1 - struct sc_uhid_devices { - struct sc_uhid_receiver *receivers[SC_UHID_MAX_RECEIVERS]; - unsigned count; + struct sc_keyboard_uhid *keyboard; }; void -sc_uhid_devices_init(struct sc_uhid_devices *devices); +sc_uhid_devices_init(struct sc_uhid_devices *devices, + struct sc_keyboard_uhid *keyboard); void -sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, - struct sc_uhid_receiver *receiver); - -struct sc_uhid_receiver * -sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id); +sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id, + const uint8_t *data, size_t size); #endif diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 50bc33fe..236a78ed 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -1,11 +1,14 @@ #include "util/log.h" #include +#include #include #include "aoa_hid.h" +#include "events.h" #include "util/log.h" #include "util/str.h" +#include "util/vector.h" // See . #define ACCESSORY_REGISTER_HID 54 @@ -15,26 +18,49 @@ #define DEFAULT_TIMEOUT 1000 -#define SC_AOA_EVENT_QUEUE_MAX 64 +// Drop droppable events above this limit +#define SC_AOA_EVENT_QUEUE_LIMIT 60 + +struct sc_vec_hid_ids SC_VECTOR(uint16_t); static void -sc_hid_event_log(uint16_t accessory_id, const struct sc_hid_event *event) { - // HID Event: [00] FF FF FF FF... - assert(event->size); - char *hex = sc_str_to_hex_string(event->data, event->size); +sc_hid_input_log(const struct sc_hid_input *hid_input) { + // HID input: [00] FF FF FF FF... + assert(hid_input->size); + char *hex = sc_str_to_hex_string(hid_input->data, hid_input->size); if (!hex) { return; } - LOGV("HID Event: [%d] %s", accessory_id, hex); + LOGV("HID input: [%" PRIu16 "] %s", hid_input->hid_id, hex); free(hex); } +static void +sc_hid_open_log(const struct sc_hid_open *hid_open) { + // HID open: [00] FF FF FF FF... + assert(hid_open->report_desc_size); + char *hex = sc_str_to_hex_string(hid_open->report_desc, + hid_open->report_desc_size); + if (!hex) { + return; + } + LOGV("HID open: [%" PRIu16 "] %s", hid_open->hid_id, hex); + free(hex); +} + +static void +sc_hid_close_log(const struct sc_hid_close *hid_close) { + // HID close: [00] + LOGV("HID close: [%" PRIu16 "]", hid_close->hid_id); +} + bool sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { sc_vecdeque_init(&aoa->queue); - if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_MAX)) { + // Add 4 to support 4 non-droppable events without re-allocation + if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_LIMIT + 4)) { return false; } @@ -125,38 +151,18 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, return true; } -bool -sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size) { - bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); - if (!ok) { - return false; - } - - ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, - report_desc_size); - if (!ok) { - if (!sc_aoa_unregister_hid(aoa, accessory_id)) { - LOGW("Could not unregister HID"); - } - return false; - } - - return true; -} - static bool -sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, - const struct sc_hid_event *event) { +sc_aoa_send_hid_event(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SEND_HID_EVENT; // // value (arg0): accessory assigned ID for the HID device // index (arg1): 0 (unused) - uint16_t value = accessory_id; + uint16_t value = hid_input->hid_id; uint16_t index = 0; - unsigned char *data = (uint8_t *) event->data; // discard const - uint16_t length = event->size; + unsigned char *data = (uint8_t *) hid_input->data; // discard const + uint16_t length = hid_input->size; int result = libusb_control_transfer(aoa->usb->handle, request_type, request, value, index, data, length, DEFAULT_TIMEOUT); @@ -169,7 +175,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, return true; } -bool +static bool sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_UNREGISTER_HID; @@ -192,41 +198,213 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { return true; } +static bool +sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size) { + bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); + if (!ok) { + return false; + } + + ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, + report_desc_size); + if (!ok) { + if (!sc_aoa_unregister_hid(aoa, accessory_id)) { + LOGW("Could not unregister HID"); + } + return false; + } + + return true; +} + bool -sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - uint16_t accessory_id, - const struct sc_hid_event *event, - uint64_t ack_to_wait) { +sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input, + uint64_t ack_to_wait) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_event_log(accessory_id, event); + sc_hid_input_log(hid_input); } sc_mutex_lock(&aoa->mutex); - bool full = sc_vecdeque_is_full(&aoa->queue); - if (!full) { + + bool pushed = false; + + size_t size = sc_vecdeque_size(&aoa->queue); + if (size < SC_AOA_EVENT_QUEUE_LIMIT) { bool was_empty = sc_vecdeque_is_empty(&aoa->queue); struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole_noresize(&aoa->queue); - aoa_event->hid = *event; - aoa_event->accessory_id = accessory_id; - aoa_event->ack_to_wait = ack_to_wait; + aoa_event->type = SC_AOA_EVENT_TYPE_INPUT; + aoa_event->input.hid = *hid_input; + aoa_event->input.ack_to_wait = ack_to_wait; + pushed = true; if (was_empty) { sc_cond_signal(&aoa->event_cond); } } - // Otherwise (if the queue is full), the event is discarded + // Otherwise, the event is discarded sc_mutex_unlock(&aoa->mutex); - return !full; + return pushed; +} + +bool +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open, + bool exit_on_open_error) { + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + sc_hid_open_log(hid_open); + } + + sc_mutex_lock(&aoa->mutex); + bool was_empty = sc_vecdeque_is_empty(&aoa->queue); + + // an OPEN event is non-droppable, so push it to the queue even above the + // SC_AOA_EVENT_QUEUE_LIMIT + struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue); + if (!aoa_event) { + LOG_OOM(); + sc_mutex_unlock(&aoa->mutex); + return false; + } + + aoa_event->type = SC_AOA_EVENT_TYPE_OPEN; + aoa_event->open.hid = *hid_open; + aoa_event->open.exit_on_error = exit_on_open_error; + + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } + + sc_mutex_unlock(&aoa->mutex); + + return true; +} + +bool +sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) { + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + sc_hid_close_log(hid_close); + } + + sc_mutex_lock(&aoa->mutex); + bool was_empty = sc_vecdeque_is_empty(&aoa->queue); + + // a CLOSE event is non-droppable, so push it to the queue even above the + // SC_AOA_EVENT_QUEUE_LIMIT + struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue); + if (!aoa_event) { + LOG_OOM(); + sc_mutex_unlock(&aoa->mutex); + return false; + } + + aoa_event->type = SC_AOA_EVENT_TYPE_CLOSE; + aoa_event->close.hid = *hid_close; + + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } + + sc_mutex_unlock(&aoa->mutex); + + return true; +} + +static bool +sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event, + struct sc_vec_hid_ids *vec_open) { + switch (event->type) { + case SC_AOA_EVENT_TYPE_INPUT: { + uint64_t ack_to_wait = event->input.ack_to_wait; + if (ack_to_wait != SC_SEQUENCE_INVALID) { + LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); + + // If some events have ack_to_wait set, then sc_aoa must have + // been initialized with a non NULL acksync + assert(aoa->acksync); + + // Do not block the loop indefinitely if the ack never comes (it + // should never happen) + sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); + enum sc_acksync_wait_result result = + sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); + + if (result == SC_ACKSYNC_WAIT_TIMEOUT) { + LOGW("Ack not received after 500ms, discarding HID event"); + // continue to process events + return true; + } else if (result == SC_ACKSYNC_WAIT_INTR) { + // stopped + return false; + } + } + + struct sc_hid_input *hid_input = &event->input.hid; + bool ok = sc_aoa_send_hid_event(aoa, hid_input); + if (!ok) { + LOGW("Could not send HID event to USB device: %" PRIu16, + hid_input->hid_id); + } + + break; + } + case SC_AOA_EVENT_TYPE_OPEN: { + struct sc_hid_open *hid_open = &event->open.hid; + bool ok = sc_aoa_setup_hid(aoa, hid_open->hid_id, + hid_open->report_desc, + hid_open->report_desc_size); + if (ok) { + // The device is now open, add it to the list of devices to + // close automatically on exit + bool pushed = sc_vector_push(vec_open, hid_open->hid_id); + if (!pushed) { + LOG_OOM(); + // this is not fatal, the HID device will just not be + // explicitly unregistered + } + } else { + LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id); + if (event->open.exit_on_error) { + // Notify the error to the main thread, which will exit + sc_push_event(SC_EVENT_AOA_OPEN_ERROR); + } + } + + break; + } + case SC_AOA_EVENT_TYPE_CLOSE: { + struct sc_hid_close *hid_close = &event->close.hid; + bool ok = sc_aoa_unregister_hid(aoa, hid_close->hid_id); + if (ok) { + // The device is not open anymore, remove it from the list of + // devices to close automatically on exit + ssize_t idx = sc_vector_index_of(vec_open, hid_close->hid_id); + if (idx >= 0) { + sc_vector_remove(vec_open, idx); + } + } else { + LOGW("Could not close AOA device: %" PRIu16, hid_close->hid_id); + } + + break; + } + } + + // continue to process events + return true; } static int run_aoa_thread(void *data) { struct sc_aoa *aoa = data; + // Store the HID ids of opened devices to unregister them all before exiting + struct sc_vec_hid_ids vec_open = SC_VECTOR_INITIALIZER; + for (;;) { sc_mutex_lock(&aoa->mutex); while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) { @@ -240,36 +418,26 @@ run_aoa_thread(void *data) { assert(!sc_vecdeque_is_empty(&aoa->queue)); struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue); - uint64_t ack_to_wait = event.ack_to_wait; sc_mutex_unlock(&aoa->mutex); - if (ack_to_wait != SC_SEQUENCE_INVALID) { - LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); - - // If some events have ack_to_wait set, then sc_aoa must have been - // initialized with a non NULL acksync - assert(aoa->acksync); - - // Do not block the loop indefinitely if the ack never comes (it - // should never happen) - sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); - enum sc_acksync_wait_result result = - sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); - - if (result == SC_ACKSYNC_WAIT_TIMEOUT) { - LOGW("Ack not received after 500ms, discarding HID event"); - continue; - } else if (result == SC_ACKSYNC_WAIT_INTR) { - // stopped - break; - } - } - - bool ok = sc_aoa_send_hid_event(aoa, event.accessory_id, &event.hid); - if (!ok) { - LOGW("Could not send HID event to USB device"); + bool cont = sc_aoa_process_event(aoa, &event, &vec_open); + if (!cont) { + // stopped + break; } } + + // Explicitly unregister all registered HID ids before exiting + for (size_t i = 0; i < vec_open.size; ++i) { + uint16_t hid_id = vec_open.data[i]; + LOGD("Unregistering AOA device %" PRIu16 "...", hid_id); + bool ok = sc_aoa_unregister_hid(aoa, hid_id); + if (!ok) { + LOGW("Could not close AOA device: %" PRIu16, hid_id); + } + } + sc_vector_destroy(&vec_open); + return 0; } diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 33a1f136..00961c28 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -13,12 +13,27 @@ #include "util/tick.h" #include "util/vecdeque.h" -#define SC_HID_MAX_SIZE 8 +enum sc_aoa_event_type { + SC_AOA_EVENT_TYPE_OPEN, + SC_AOA_EVENT_TYPE_INPUT, + SC_AOA_EVENT_TYPE_CLOSE, +}; struct sc_aoa_event { - struct sc_hid_event hid; - uint16_t accessory_id; - uint64_t ack_to_wait; + enum sc_aoa_event_type type; + union { + struct { + struct sc_hid_open hid; + bool exit_on_error; + } open; + struct { + struct sc_hid_close hid; + } close; + struct { + struct sc_hid_input hid; + uint64_t ack_to_wait; + } input; + }; }; struct sc_aoa_event_queue SC_VECDEQUE(struct sc_aoa_event); @@ -49,24 +64,31 @@ sc_aoa_stop(struct sc_aoa *aoa); void sc_aoa_join(struct sc_aoa *aoa); +//bool +//sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, +// const uint8_t *report_desc, uint16_t report_desc_size); +// +//bool +//sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); + +// report_desc must be a pointer to static memory, accessed at any time from +// another thread bool -sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size); +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open, + bool exit_on_open_error); bool -sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); +sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close); bool -sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - uint16_t accessory_id, - const struct sc_hid_event *event, - uint64_t ack_to_wait); +sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input, + uint64_t ack_to_wait); static inline bool -sc_aoa_push_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, - const struct sc_hid_event *event) { - return sc_aoa_push_hid_event_with_ack_to_wait(aoa, accessory_id, event, - SC_SEQUENCE_INVALID); +sc_aoa_push_input(struct sc_aoa *aoa, const struct sc_hid_input *hid_input) { + return sc_aoa_push_input_with_ack_to_wait(aoa, hid_input, + SC_SEQUENCE_INVALID); } #endif diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c new file mode 100644 index 00000000..37587532 --- /dev/null +++ b/app/src/usb/gamepad_aoa.c @@ -0,0 +1,91 @@ +#include "gamepad_aoa.h" + +#include "input_events.h" +#include "util/log.h" + +/** Downcast gamepad processor to gamepad_aoa */ +#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor) + +static void +sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + if (event->type == SC_GAMEPAD_DEVICE_ADDED) { + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; + } + + // exit_on_error: false (a gamepad open failure should not exit scrcpy) + if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { + LOGW("Could not push AOA HID open (gamepad)"); + } + } else { + assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); + + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { + LOGW("Could not push AOA HID close (gamepad)"); + } + } +} + +static void +sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, + event)) { + return; + } + + if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (gamepad axis)"); + } +} + +static void +sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp, + const struct sc_gamepad_button_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, + event)) { + return; + } + + if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (gamepad button)"); + } +} + +void +sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) { + gamepad->aoa = aoa; + + sc_hid_gamepad_init(&gamepad->hid); + + static const struct sc_gamepad_processor_ops ops = { + .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, + .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, + }; + + gamepad->gamepad_processor.ops = &ops; +} + +void +sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad) { + (void) gamepad; + // Do nothing, gamepad->aoa will automatically unregister all devices +} diff --git a/app/src/usb/gamepad_aoa.h b/app/src/usb/gamepad_aoa.h new file mode 100644 index 00000000..b2dfbe5e --- /dev/null +++ b/app/src/usb/gamepad_aoa.h @@ -0,0 +1,25 @@ +#ifndef SC_GAMEPAD_AOA_H +#define SC_GAMEPAD_AOA_H + +#include "common.h" + +#include + +#include "aoa_hid.h" +#include "hid/hid_gamepad.h" +#include "trait/gamepad_processor.h" + +struct sc_gamepad_aoa { + struct sc_gamepad_processor gamepad_processor; // gamepad processor trait + + struct sc_hid_gamepad hid; + struct sc_aoa *aoa; +}; + +void +sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa); + +void +sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad); + +#endif diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 736c97b0..8f5cb755 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -8,19 +8,16 @@ /** Downcast key processor to keyboard_aoa */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_aoa, key_processor) -#define HID_KEYBOARD_ACCESSORY_ID 1 - static bool push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { - struct sc_hid_event hid_event; - if (!sc_hid_keyboard_event_from_mods(&hid_event, mods_state)) { + struct sc_hid_input hid_input; + if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, mods_state)) { // Nothing to do return true; } - if (!sc_aoa_push_hid_event(kb->aoa, HID_KEYBOARD_ACCESSORY_ID, - &hid_event)) { - LOGW("Could not request HID event (mod lock state)"); + if (!sc_aoa_push_input(kb->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (mod lock state)"); return false; } @@ -41,10 +38,10 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct sc_keyboard_aoa *kb = DOWNCAST(kp); - struct sc_hid_event hid_event; + struct sc_hid_input hid_input; // Not all keys are supported, just ignore unsupported keys - if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) { if (!kb->mod_lock_synchronized) { // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize // keyboard state @@ -58,11 +55,9 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // synchronization is acknowledged by the server, otherwise it could // paste the old clipboard content. - if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, - HID_KEYBOARD_ACCESSORY_ID, - &hid_event, - ack_to_wait)) { - LOGW("Could not request HID event (key)"); + if (!sc_aoa_push_input_with_ack_to_wait(kb->aoa, &hid_input, + ack_to_wait)) { + LOGW("Could not push AOA HID input (key)"); } } } @@ -71,11 +66,12 @@ bool sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { kb->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID, - SC_HID_KEYBOARD_REPORT_DESC, - SC_HID_KEYBOARD_REPORT_DESC_LEN); + struct sc_hid_open hid_open; + sc_hid_keyboard_generate_open(&hid_open); + + bool ok = sc_aoa_push_open(aoa, &hid_open, true); if (!ok) { - LOGW("Register HID keyboard failed"); + LOGW("Could not push AOA HID open (keyboard)"); return false; } @@ -102,9 +98,6 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { - // Unregister HID keyboard so the soft keyboard shows again on Android - bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID); - if (!ok) { - LOGW("Could not unregister HID keyboard"); - } + (void) kb; + // Do nothing, kb->aoa will automatically unregister all devices } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 93b32328..cb566cc0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -9,19 +9,16 @@ /** Downcast mouse processor to mouse_aoa */ #define DOWNCAST(MP) container_of(MP, struct sc_mouse_aoa, mouse_processor) -#define HID_MOUSE_ACCESSORY_ID 2 - static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_motion(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_motion(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { - LOGW("Could not request HID event (mouse motion)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (mouse motion)"); } } @@ -30,12 +27,11 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_click(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_click(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { - LOGW("Could not request HID event (mouse click)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (mouse click)"); } } @@ -44,12 +40,11 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_scroll(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { - LOGW("Could not request HID event (mouse scroll)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (mouse scroll)"); } } @@ -57,11 +52,12 @@ bool sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { mouse->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, - SC_HID_MOUSE_REPORT_DESC, - SC_HID_MOUSE_REPORT_DESC_LEN); + struct sc_hid_open hid_open; + sc_hid_mouse_generate_open(&hid_open); + + bool ok = sc_aoa_push_open(aoa, &hid_open, true); if (!ok) { - LOGW("Register HID mouse failed"); + LOGW("Could not push AOA HID open (mouse)"); return false; } @@ -82,8 +78,6 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); - if (!ok) { - LOGW("Could not unregister HID mouse"); - } + (void) mouse; + // Do nothing, mouse->aoa will automatically unregister all devices } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index c1d38da3..9595face 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -12,6 +12,7 @@ struct scrcpy_otg { struct sc_aoa aoa; struct sc_keyboard_aoa keyboard; struct sc_mouse_aoa mouse; + struct sc_gamepad_aoa gamepad; struct sc_screen_otg screen_otg; }; @@ -21,12 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { (void) usb; (void) userdata; - SDL_Event event; - event.type = SC_EVENT_USB_DEVICE_DISCONNECTED; - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGE("Could not post USB disconnection event: %s", SDL_GetError()); - } + sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED); } static enum scrcpy_exit_code @@ -37,6 +33,9 @@ event_loop(struct scrcpy_otg *s) { case SC_EVENT_USB_DEVICE_DISCONNECTED: LOGW("Device disconnected"); return SCRCPY_EXIT_DISCONNECTED; + case SC_EVENT_AOA_OPEN_ERROR: + LOGE("AOA open error"); + return SCRCPY_EXIT_FAILURE; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; @@ -59,12 +58,23 @@ scrcpy_otg(struct scrcpy_options *options) { LOGW("Could not enable linear filtering"); } + if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { + LOGW("Could not allow joystick background events"); + } + // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); return SCRCPY_EXIT_FAILURE; } + if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { + LOGE("Could not initialize SDL controller: %s", SDL_GetError()); + // Not fatal, keyboard/mouse should still work + } + } + atexit(SDL_Quit); if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { @@ -75,6 +85,7 @@ scrcpy_otg(struct scrcpy_options *options) { struct sc_keyboard_aoa *keyboard = NULL; struct sc_mouse_aoa *mouse = NULL; + struct sc_gamepad_aoa *gamepad = NULL; bool usb_device_initialized = false; bool usb_connected = false; bool aoa_started = false; @@ -121,11 +132,15 @@ scrcpy_otg(struct scrcpy_options *options) { || options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_DISABLED); assert(options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA || options->mouse_input_mode == SC_MOUSE_INPUT_MODE_DISABLED); + assert(options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA + || options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_DISABLED); bool enable_keyboard = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; bool enable_mouse = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; + bool enable_gamepad = + options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; if (enable_keyboard) { ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa); @@ -143,6 +158,11 @@ scrcpy_otg(struct scrcpy_options *options) { mouse = &s->mouse; } + if (enable_gamepad) { + sc_gamepad_aoa_init(&s->gamepad, &s->aoa); + gamepad = &s->gamepad; + } + ok = sc_aoa_start(&s->aoa); if (!ok) { goto end; @@ -157,6 +177,7 @@ scrcpy_otg(struct scrcpy_options *options) { struct sc_screen_otg_params params = { .keyboard = keyboard, .mouse = mouse, + .gamepad = gamepad, .window_title = window_title, .always_on_top = options->always_on_top, .window_x = options->window_x, @@ -190,6 +211,9 @@ end: if (keyboard) { sc_keyboard_aoa_destroy(&s->keyboard); } + if (gamepad) { + sc_gamepad_aoa_destroy(&s->gamepad); + } if (aoa_initialized) { sc_aoa_join(&s->aoa); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 5c4f97f0..b13f8d04 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -59,6 +59,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, const struct sc_screen_otg_params *params) { screen->keyboard = params->keyboard; screen->mouse = params->mouse; + screen->gamepad = params->gamepad; screen->mouse_capture_key_pressed = 0; @@ -214,6 +215,87 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, mp->ops->process_mouse_scroll(mp, &evt); } +static void +sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, + const SDL_ControllerDeviceEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + SDL_JoystickID id; + if (event->type == SDL_CONTROLLERDEVICEADDED) { + SDL_GameController *gc = SDL_GameControllerOpen(event->which); + if (!gc) { + LOGW("Could not open game controller"); + return; + } + + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc); + if (!joystick) { + LOGW("Could not get controller joystick"); + SDL_GameControllerClose(gc); + return; + } + + id = SDL_JoystickInstanceID(joystick); + } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + id = event->which; + + SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); + if (gc) { + SDL_GameControllerClose(gc); + } else { + LOGW("Unknown gamepad device removed"); + } + } else { + // Nothing to do + return; + } + + struct sc_gamepad_device_event evt = { + .type = sc_gamepad_device_event_type_from_sdl_type(event->type), + .gamepad_id = id, + }; + gp->ops->process_gamepad_device(gp, &evt); +} + +static void +sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen, + const SDL_ControllerAxisEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis); + if (axis == SC_GAMEPAD_AXIS_UNKNOWN) { + return; + } + + struct sc_gamepad_axis_event evt = { + .gamepad_id = event->which, + .axis = axis, + .value = event->value, + }; + gp->ops->process_gamepad_axis(gp, &evt); +} + +static void +sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen, + const SDL_ControllerButtonEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button); + if (button == SC_GAMEPAD_BUTTON_UNKNOWN) { + return; + } + + struct sc_gamepad_button_event evt = { + .gamepad_id = event->which, + .action = sc_action_from_sdl_controllerbutton_type(event->type), + .button = button, + }; + gp->ops->process_gamepad_button(gp, &evt); +} + void sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { switch (event->type) { @@ -293,5 +375,23 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + // Handle device added or removed even if paused + if (screen->gamepad) { + sc_screen_otg_process_gamepad_device(screen, &event->cdevice); + } + break; + case SDL_CONTROLLERAXISMOTION: + if (screen->gamepad) { + sc_screen_otg_process_gamepad_axis(screen, &event->caxis); + } + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (screen->gamepad) { + sc_screen_otg_process_gamepad_button(screen, &event->cbutton); + } + break; } } diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index c4e03b87..2ea76eda 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -8,10 +8,12 @@ #include "keyboard_aoa.h" #include "mouse_aoa.h" +#include "gamepad_aoa.h" struct sc_screen_otg { struct sc_keyboard_aoa *keyboard; struct sc_mouse_aoa *mouse; + struct sc_gamepad_aoa *gamepad; SDL_Window *window; SDL_Renderer *renderer; @@ -24,6 +26,7 @@ struct sc_screen_otg { struct sc_screen_otg_params { struct sc_keyboard_aoa *keyboard; struct sc_mouse_aoa *mouse; + struct sc_gamepad_aoa *gamepad; const char *window_title; bool always_on_top; diff --git a/app/src/util/binary.h b/app/src/util/binary.h index 6dc1b58e..7de9b505 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -13,6 +13,12 @@ sc_write16be(uint8_t *buf, uint16_t value) { buf[1] = value; } +static inline void +sc_write16le(uint8_t *buf, uint16_t value) { + buf[0] = value; + buf[1] = value >> 8; +} + static inline void sc_write32be(uint8_t *buf, uint32_t value) { buf[0] = value >> 24; @@ -21,12 +27,26 @@ sc_write32be(uint8_t *buf, uint32_t value) { buf[3] = value; } +static inline void +sc_write32le(uint8_t *buf, uint32_t value) { + buf[0] = value; + buf[1] = value >> 8; + buf[2] = value >> 16; + buf[3] = value >> 24; +} + static inline void sc_write64be(uint8_t *buf, uint64_t value) { sc_write32be(buf, value >> 32); sc_write32be(&buf[4], (uint32_t) value); } +static inline void +sc_write64le(uint8_t *buf, uint64_t value) { + sc_write32le(buf, (uint32_t) value); + sc_write32le(&buf[4], value >> 32); +} + static inline uint16_t sc_read16be(const uint8_t *buf) { return (buf[0] << 8) | buf[1]; diff --git a/app/src/util/net.c b/app/src/util/net.c index 67317ead..d43d1c7a 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -15,6 +15,7 @@ # include # include # include +# include # include # include # include @@ -273,6 +274,22 @@ net_close(sc_socket socket) { #endif } +bool +net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay) { + sc_raw_socket raw_sock = unwrap(socket); + + int value = tcp_nodelay ? 1 : 0; + int ret = setsockopt(raw_sock, IPPROTO_TCP, TCP_NODELAY, + (const void *) &value, sizeof(value)); + if (ret == -1) { + net_perror("setsockopt(TCP_NODELAY)"); + return false; + } + + assert(ret == 0); + return true; +} + bool net_parse_ipv4(const char *s, uint32_t *ipv4) { struct in_addr addr; diff --git a/app/src/util/net.h b/app/src/util/net.h index 21396882..ea54b793 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -67,6 +67,10 @@ net_interrupt(sc_socket socket); bool net_close(sc_socket socket); +// Disable Nagle's algorithm (if tcp_nodelay is true) +bool +net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay); + /** * Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation */ diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 94921fb7..9679dfff 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -6,6 +6,8 @@ #include "log.h" +sc_thread_id SC_MAIN_THREAD_ID; + bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata) { diff --git a/app/src/util/thread.h b/app/src/util/thread.h index 4183adac..3d544046 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -39,6 +39,8 @@ typedef struct sc_cond { SDL_cond *cond; } sc_cond; +extern sc_thread_id SC_MAIN_THREAD_ID; + bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata); diff --git a/app/tests/test_binary.c b/app/tests/test_binary.c index 82a9c1e0..bce74ce2 100644 --- a/app/tests/test_binary.c +++ b/app/tests/test_binary.c @@ -42,6 +42,44 @@ static void test_write64be(void) { assert(buf[7] == 0xEF); } +static void test_write16le(void) { + uint16_t val = 0xABCD; + uint8_t buf[2]; + + sc_write16le(buf, val); + + assert(buf[0] == 0xCD); + assert(buf[1] == 0xAB); +} + +static void test_write32le(void) { + uint32_t val = 0xABCD1234; + uint8_t buf[4]; + + sc_write32le(buf, val); + + assert(buf[0] == 0x34); + assert(buf[1] == 0x12); + assert(buf[2] == 0xCD); + assert(buf[3] == 0xAB); +} + +static void test_write64le(void) { + uint64_t val = 0xABCD1234567890EF; + uint8_t buf[8]; + + sc_write64le(buf, val); + + assert(buf[0] == 0xEF); + assert(buf[1] == 0x90); + assert(buf[2] == 0x78); + assert(buf[3] == 0x56); + assert(buf[4] == 0x34); + assert(buf[5] == 0x12); + assert(buf[6] == 0xCD); + assert(buf[7] == 0xAB); +} + static void test_read16be(void) { uint8_t buf[2] = {0xAB, 0xCD}; @@ -108,6 +146,10 @@ int main(int argc, char *argv[]) { test_read32be(); test_read64be(); + test_write16le(); + test_write32le(); + test_write64le(); + test_float_to_u16fp(); test_float_to_i16fp(); return 0; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index cef8df3e..14765792 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -78,7 +78,7 @@ static void test_options(void) { assert(opts->video_bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); - assert(opts->max_fps == 30); + assert(!strcmp(opts->max_fps, "30")); assert(opts->max_size == 1024); assert(opts->lock_video_orientation == 2); assert(opts->port_range.first == 1234); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 7a978f2b..72ec61ee 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -329,6 +329,7 @@ static void test_serialize_uhid_create(void) { .type = SC_CONTROL_MSG_TYPE_UHID_CREATE, .uhid_create = { .id = 42, + .name = "ABC", .report_desc_size = sizeof(report_desc), .report_desc = report_desc, }, @@ -336,12 +337,14 @@ static void test_serialize_uhid_create(void) { uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 16); + assert(size == 20); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_UHID_CREATE, 0, 42, // id - 0, 11, // size + 3, // name size + 65, 66, 67, // "ABC" + 0, 11, // report desc size 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -370,6 +373,25 @@ static void test_serialize_uhid_input(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_uhid_destroy(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_UHID_DESTROY, + .uhid_destroy = { + .id = 42, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 3); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_DESTROY, + 0, 42, // id + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_open_hard_keyboard(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, @@ -405,6 +427,7 @@ int main(int argc, char *argv[]) { test_serialize_rotate_device(); test_serialize_uhid_create(); test_serialize_uhid_input(); + test_serialize_uhid_destroy(); test_serialize_open_hard_keyboard(); return 0; } diff --git a/doc/build.md b/doc/build.md index 15e0ffff..63bd7ca7 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.6.1`][direct-scrcpy-server] - SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b` + - [`scrcpy-server-v2.7`][direct-scrcpy-server] + SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/gamepad.md b/doc/gamepad.md new file mode 100644 index 00000000..d3d27b51 --- /dev/null +++ b/doc/gamepad.md @@ -0,0 +1,58 @@ +# Gamepad + +Several gamepad input modes are available: + + - `--gamepad=disabled` (default) + - `--gamepad=uhid` (or `-G`): simulates physical HID gamepads using the UHID + kernel module on the device + - `--gamepad=aoa`: simulates physical HID gamepads using the AOAv2 protocol + + +## Physical gamepad simulation + +Two modes allow to simulate physical HID gamepads on the device, one for each +physical gamepad plugged into the computer. + + +### UHID + +This mode simulates physical HID gamepads using the [UHID] kernel module on the +device. + +[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt + +To enable UHID gamepads, use: + +```bash +scrcpy --gamepad=uhid +scrcpy -G # short version +``` + +Note: UHID may not work on old Android versions due to permission errors. + + +### AOA + +This mode simulates physical HID gamepads using the [AOAv2] protocol. + +[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support + +To enable AOA gamepads, use: + +```bash +scrcpy --gamepad=aoa +``` + +Contrary to the other mode, it works at the USB level directly (so it only works +over USB). + +It does not use the scrcpy server, and does not require `adb` (USB debugging). +Therefore, it is possible to control the device (but not mirror) even with USB +debugging disabled (see [OTG](otg.md)). + +Note: For some reason, in this mode, Android detects multiple physical gamepads +as a single misbehaving one. Use UHID if you need multiple gamepads. + +Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring +(it is not possible to open a USB device if it is already open by another +process like the _adb daemon_). diff --git a/doc/mouse.md b/doc/mouse.md index ec4aea63..ae7c6834 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -53,6 +53,8 @@ scrcpy --mouse=uhid scrcpy -M # short version ``` +Note: UHID may not work on old Android versions due to permission errors. + ### AOA diff --git a/doc/otg.md b/doc/otg.md index c9107e11..7d31c0a7 100644 --- a/doc/otg.md +++ b/doc/otg.md @@ -6,16 +6,18 @@ was a [physical keyboard] and/or a [physical mouse] connected to the Android device (see [keyboard](keyboard.md) and [mouse](mouse.md)). [physical keyboard]: keyboard.md#physical-keyboard-simulation -[physical mouse]: physical-keyboard-simulation +[physical mouse]: mouse.md#physical-mouse-simulation A special mode (OTG) allows to control the device using AOA -[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at -all (so USB debugging is not necessary). In this mode, video and audio are -disabled, and `--keyboard=aoa` and `--mouse=aoa` are implicitly set. +[keyboard](keyboard.md#aoa), [mouse](mouse.md#aoa) and +[gamepad](gamepad.md#aoa), without using _adb_ at all (so USB debugging is not +necessary). In this mode, video and audio are disabled, and `--keyboard=aoa` and +`--mouse=aoa` are implicitly set. However, gamepads are disabled by default, so +`--gamepad=aoa` (or `-G` in OTG mode) must be explicitly set. -Therefore, it is possible to run _scrcpy_ with only physical keyboard and mouse -simulation, as if the computer keyboard and mouse were plugged directly to the -device via an OTG cable. +Therefore, it is possible to run _scrcpy_ with only physical keyboard, mouse and +gamepad simulation, as if the computer keyboard, mouse and gamepads were plugged +directly to the device via an OTG cable. To enable OTG mode: @@ -32,6 +34,13 @@ scrcpy --otg --keyboard=disabled scrcpy --otg --mouse=disabled ``` +and to enable gamepads: + +```bash +scrcpy --otg --gamepad=aoa +scrcpy --otg -G # short version +``` + It only works if the device is connected over USB. ## OTG issues on Windows @@ -50,9 +59,9 @@ is enabled, then OTG mode is not necessary. Instead, disable video and audio, and select UHID (or AOA): ```bash -scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid -scrcpy --no-video --no-audio -KM # short version -scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa +scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid --gamepad=uhid +scrcpy --no-video --no-audio -KMG # short version +scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa --gamepad=aoa ``` One benefit of UHID is that it also works wirelessly. diff --git a/doc/windows.md b/doc/windows.md index 65ec2b45..36e59178 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`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` + - [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit) + SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5` + - [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit) + SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[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 +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 2aad8cdc..3cf3490c 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 -PREBUILT_SERVER_SHA256=ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 +PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server diff --git a/meson.build b/meson.build index b532006a..f76d5ecf 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.6.1', + version: '2.7', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/release.mk b/release.mk index dd544bae..7f082144 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 --exclude='*install-release' --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 decacd3f..655298a9 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20601 - versionName "2.6.1" + versionCode 20700 + versionName "2.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5ee7af30..ab6c821d 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.6.1 +SCRCPY_VERSION_NAME=2.7 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 2f86d8ce..51daeced 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -29,7 +29,7 @@ public class Options { private boolean audioDup; private int videoBitRate = 8000000; private int audioBitRate = 128000; - private int maxFps; + private float maxFps; private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; @@ -113,7 +113,7 @@ public class Options { return audioBitRate; } - public int getMaxFps() { + public float getMaxFps() { return maxFps; } @@ -321,7 +321,7 @@ public class Options { options.audioBitRate = Integer.parseInt(value); break; case "max_fps": - options.maxFps = Integer.parseInt(value); + options.maxFps = parseFloat("max_fps", value); break; case "lock_video_orientation": options.lockVideoOrientation = Integer.parseInt(value); @@ -456,8 +456,14 @@ public class Options { } int width = Integer.parseInt(tokens[0]); int height = Integer.parseInt(tokens[1]); + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Invalid crop size: " + width + "x" + height); + } int x = Integer.parseInt(tokens[2]); int y = Integer.parseInt(tokens[3]); + if (x < 0 || y < 0) { + throw new IllegalArgumentException("Invalid crop offset: " + x + ":" + y); + } return new Rect(x, y, x + width, y + height); } @@ -487,4 +493,12 @@ public class Options { float floatAr = Float.parseFloat(tokens[0]); return CameraAspectRatio.fromFloat(floatAr); } + + private static float parseFloat(String key, String value) { + try { + return Float.parseFloat(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\""); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 8fc38555..7de98b72 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -61,7 +61,14 @@ public final class Workarounds { fillConfigurationController(); } - fillAppInfo(); + // On ONYX devices, fillAppInfo() breaks video mirroring: + // + boolean mustFillAppInfo = !Build.BRAND.equalsIgnoreCase("ONYX"); + + if (mustFillAppInfo) { + fillAppInfo(); + } + fillAppContext(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 361c7bac..8d4a4c2d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -56,8 +56,11 @@ public class AudioDirectCapture implements AudioCapture { builder.setAudioSource(audioSource); builder.setAudioFormat(AudioConfig.createAudioFormat()); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); - // This buffer size does not impact latency - builder.setBufferSizeInBytes(8 * minBufferSize); + if (minBufferSize > 0) { + // This buffer size does not impact latency + builder.setBufferSizeInBytes(8 * minBufferSize); + } + return builder.build(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java index f24ca117..2f12cdb3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java @@ -3,31 +3,22 @@ package com.genymobile.scrcpy.control; import android.net.LocalSocket; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; public final class ControlChannel { - private final InputStream inputStream; - private final OutputStream outputStream; - private final ControlMessageReader reader = new ControlMessageReader(); - private final DeviceMessageWriter writer = new DeviceMessageWriter(); + private final ControlMessageReader reader; + private final DeviceMessageWriter writer; public ControlChannel(LocalSocket controlSocket) throws IOException { - this.inputStream = controlSocket.getInputStream(); - this.outputStream = controlSocket.getOutputStream(); + reader = new ControlMessageReader(controlSocket.getInputStream()); + writer = new DeviceMessageWriter(controlSocket.getOutputStream()); } public ControlMessage recv() throws IOException { - ControlMessage msg = reader.next(); - while (msg == null) { - reader.readFrom(inputStream); - msg = reader.next(); - } - return msg; + return reader.read(); } public void send(DeviceMessage msg) throws IOException { - writer.writeTo(msg, outputStream); + writer.write(msg); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index c414f2a5..d1406ed0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -21,7 +21,8 @@ public final class ControlMessage { public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_INPUT = 13; - public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 14; + public static final int TYPE_UHID_DESTROY = 14; + public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final long SEQUENCE_INVALID = 0; @@ -130,10 +131,11 @@ public final class ControlMessage { return msg; } - public static ControlMessage createUhidCreate(int id, byte[] reportDesc) { + public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_UHID_CREATE; msg.id = id; + msg.text = name; msg.data = reportDesc; return msg; } @@ -146,6 +148,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createUhidDestroy(int id) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_UHID_DESTROY; + msg.id = id; + return msg; + } + public int getType() { return type; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index f5cfee75..45116935 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -1,259 +1,165 @@ 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.BufferedInputStream; +import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class ControlMessageReader { - static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; - static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31; - static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; - static final int BACK_OR_SCREEN_ON_LENGTH = 1; - static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; - static final int GET_CLIPBOARD_LENGTH = 1; - static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; - static final int UHID_CREATE_FIXED_PAYLOAD_LENGTH = 4; - static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 4; - private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes public static final int INJECT_TEXT_MAX_LENGTH = 300; - private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; - private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final DataInputStream dis; - public ControlMessageReader() { - // invariant: the buffer is always in "get" mode - buffer.limit(0); + public ControlMessageReader(InputStream rawInputStream) { + dis = new DataInputStream(new BufferedInputStream(rawInputStream)); } - public boolean isFull() { - return buffer.remaining() == rawBuffer.length; - } - - public void readFrom(InputStream input) throws IOException { - if (isFull()) { - throw new IllegalStateException("Buffer full, call next() to consume"); - } - buffer.compact(); - int head = buffer.position(); - int r = input.read(rawBuffer, head, rawBuffer.length - head); - if (r == -1) { - throw new EOFException("Controller socket closed"); - } - buffer.position(head + r); - buffer.flip(); - } - - public ControlMessage next() { - if (!buffer.hasRemaining()) { - return null; - } - int savedPosition = buffer.position(); - - int type = buffer.get(); - ControlMessage msg; + public ControlMessage read() throws IOException { + int type = dis.readUnsignedByte(); switch (type) { case ControlMessage.TYPE_INJECT_KEYCODE: - msg = parseInjectKeycode(); - break; + return parseInjectKeycode(); case ControlMessage.TYPE_INJECT_TEXT: - msg = parseInjectText(); - break; + return parseInjectText(); case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - msg = parseInjectTouchEvent(); - break; + return parseInjectTouchEvent(); case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - msg = parseInjectScrollEvent(); - break; + return parseInjectScrollEvent(); case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - msg = parseBackOrScreenOnEvent(); - break; + return parseBackOrScreenOnEvent(); case ControlMessage.TYPE_GET_CLIPBOARD: - msg = parseGetClipboard(); - break; + return parseGetClipboard(); case ControlMessage.TYPE_SET_CLIPBOARD: - msg = parseSetClipboard(); - break; + return parseSetClipboard(); case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - msg = parseSetScreenPowerMode(); - break; + return parseSetScreenPowerMode(); case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: - msg = ControlMessage.createEmpty(type); - break; + return ControlMessage.createEmpty(type); case ControlMessage.TYPE_UHID_CREATE: - msg = parseUhidCreate(); - break; + return parseUhidCreate(); case ControlMessage.TYPE_UHID_INPUT: - msg = parseUhidInput(); - break; + return parseUhidInput(); + case ControlMessage.TYPE_UHID_DESTROY: + return parseUhidDestroy(); default: - Ln.w("Unknown event type: " + type); - msg = null; - break; + throw new ControlProtocolException("Unknown event type: " + type); } - - if (msg == null) { - // failure, reset savedPosition - buffer.position(savedPosition); - } - return msg; } - private ControlMessage parseInjectKeycode() { - if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); - int keycode = buffer.getInt(); - int repeat = buffer.getInt(); - int metaState = buffer.getInt(); + private ControlMessage parseInjectKeycode() throws IOException { + int action = dis.readUnsignedByte(); + int keycode = dis.readInt(); + int repeat = dis.readInt(); + int metaState = dis.readInt(); return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } - private int parseBufferLength(int sizeBytes) { + private int parseBufferLength(int sizeBytes) throws IOException { assert sizeBytes > 0 && sizeBytes <= 4; - if (buffer.remaining() < sizeBytes) { - return -1; - } int value = 0; for (int i = 0; i < sizeBytes; ++i) { - value = (value << 8) | (buffer.get() & 0xFF); + value = (value << 8) | dis.readUnsignedByte(); } return value; } - private String parseString() { - int len = parseBufferLength(4); - if (len == -1 || buffer.remaining() < len) { - return null; - } - int position = buffer.position(); - // Move the buffer position to consume the text - buffer.position(position + len); - return new String(rawBuffer, position, len, StandardCharsets.UTF_8); + private String parseString(int sizeBytes) throws IOException { + assert sizeBytes > 0 && sizeBytes <= 4; + byte[] data = parseByteArray(sizeBytes); + return new String(data, StandardCharsets.UTF_8); } - private byte[] parseByteArray(int sizeBytes) { + private String parseString() throws IOException { + return parseString(4); + } + + private byte[] parseByteArray(int sizeBytes) throws IOException { int len = parseBufferLength(sizeBytes); - if (len == -1 || buffer.remaining() < len) { - return null; - } byte[] data = new byte[len]; - buffer.get(data); + dis.readFully(data); return data; } - private ControlMessage parseInjectText() { + private ControlMessage parseInjectText() throws IOException { String text = parseString(); - if (text == null) { - return null; - } return ControlMessage.createInjectText(text); } - private ControlMessage parseInjectTouchEvent() { - if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); - long pointerId = buffer.getLong(); - Position position = readPosition(buffer); - float pressure = Binary.u16FixedPointToFloat(buffer.getShort()); - int actionButton = buffer.getInt(); - int buttons = buffer.getInt(); + private ControlMessage parseInjectTouchEvent() throws IOException { + int action = dis.readUnsignedByte(); + long pointerId = dis.readLong(); + Position position = parsePosition(); + float pressure = Binary.u16FixedPointToFloat(dis.readShort()); + int actionButton = dis.readInt(); + int buttons = dis.readInt(); return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons); } - private ControlMessage parseInjectScrollEvent() { - if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { - return null; - } - Position position = readPosition(buffer); - float hScroll = Binary.i16FixedPointToFloat(buffer.getShort()); - float vScroll = Binary.i16FixedPointToFloat(buffer.getShort()); - int buttons = buffer.getInt(); + private ControlMessage parseInjectScrollEvent() throws IOException { + Position position = parsePosition(); + float hScroll = Binary.i16FixedPointToFloat(dis.readShort()); + float vScroll = Binary.i16FixedPointToFloat(dis.readShort()); + int buttons = dis.readInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } - private ControlMessage parseBackOrScreenOnEvent() { - if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); + private ControlMessage parseBackOrScreenOnEvent() throws IOException { + int action = dis.readUnsignedByte(); return ControlMessage.createBackOrScreenOn(action); } - private ControlMessage parseGetClipboard() { - if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { - return null; - } - int copyKey = Binary.toUnsigned(buffer.get()); + private ControlMessage parseGetClipboard() throws IOException { + int copyKey = dis.readUnsignedByte(); return ControlMessage.createGetClipboard(copyKey); } - private ControlMessage parseSetClipboard() { - if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { - return null; - } - long sequence = buffer.getLong(); - boolean paste = buffer.get() != 0; + private ControlMessage parseSetClipboard() throws IOException { + long sequence = dis.readLong(); + boolean paste = dis.readByte() != 0; String text = parseString(); - if (text == null) { - return null; - } return ControlMessage.createSetClipboard(sequence, text, paste); } - private ControlMessage parseSetScreenPowerMode() { - if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { - return null; - } - int mode = buffer.get(); + private ControlMessage parseSetScreenPowerMode() throws IOException { + int mode = dis.readUnsignedByte(); return ControlMessage.createSetScreenPowerMode(mode); } - private ControlMessage parseUhidCreate() { - if (buffer.remaining() < UHID_CREATE_FIXED_PAYLOAD_LENGTH) { - return null; - } - int id = buffer.getShort(); + private ControlMessage parseUhidCreate() throws IOException { + int id = dis.readUnsignedShort(); + String name = parseString(1); byte[] data = parseByteArray(2); - if (data == null) { - return null; - } - return ControlMessage.createUhidCreate(id, data); + return ControlMessage.createUhidCreate(id, name, data); } - private ControlMessage parseUhidInput() { - if (buffer.remaining() < UHID_INPUT_FIXED_PAYLOAD_LENGTH) { - return null; - } - int id = buffer.getShort(); + private ControlMessage parseUhidInput() throws IOException { + int id = dis.readUnsignedShort(); byte[] data = parseByteArray(2); - if (data == null) { - return null; - } return ControlMessage.createUhidInput(id, data); } - private static Position readPosition(ByteBuffer buffer) { - int x = buffer.getInt(); - int y = buffer.getInt(); - int screenWidth = Binary.toUnsigned(buffer.getShort()); - int screenHeight = Binary.toUnsigned(buffer.getShort()); + private ControlMessage parseUhidDestroy() throws IOException { + int id = dis.readUnsignedShort(); + return ControlMessage.createUhidDestroy(id); + } + + private Position parsePosition() throws IOException { + int x = dis.readInt(); + int y = dis.readInt(); + int screenWidth = dis.readUnsignedShort(); + int screenHeight = dis.readUnsignedShort(); return new Position(x, y, screenWidth, screenHeight); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java new file mode 100644 index 00000000..cabf63ee --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java @@ -0,0 +1,9 @@ +package com.genymobile.scrcpy.control; + +import java.io.IOException; + +public class ControlProtocolException extends IOException { + public ControlProtocolException(String message) { + super(message); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 1494c10a..38251655 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -210,11 +210,14 @@ public class Controller implements AsyncProcessor { device.rotateDevice(); break; case ControlMessage.TYPE_UHID_CREATE: - getUhidManager().open(msg.getId(), msg.getData()); + getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); break; case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); break; + case ControlMessage.TYPE_UHID_DESTROY: + getUhidManager().close(msg.getId()); + break; case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: openHardKeyboardSettings(); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java index 6bf53bed..a18a2e5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java @@ -1,11 +1,11 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { @@ -13,35 +13,35 @@ public class DeviceMessageWriter { private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes - private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; - private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final DataOutputStream dos; - public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { - buffer.clear(); - buffer.put((byte) msg.getType()); - switch (msg.getType()) { + public DeviceMessageWriter(OutputStream rawOutputStream) { + dos = new DataOutputStream(new BufferedOutputStream(rawOutputStream)); + } + + public void write(DeviceMessage msg) throws IOException { + int type = msg.getType(); + dos.writeByte(type); + switch (type) { case DeviceMessage.TYPE_CLIPBOARD: String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); - buffer.putInt(len); - buffer.put(raw, 0, len); - output.write(rawBuffer, 0, buffer.position()); + dos.writeInt(len); + dos.write(raw, 0, len); break; case DeviceMessage.TYPE_ACK_CLIPBOARD: - buffer.putLong(msg.getSequence()); - output.write(rawBuffer, 0, buffer.position()); + dos.writeLong(msg.getSequence()); break; case DeviceMessage.TYPE_UHID_OUTPUT: - buffer.putShort((short) msg.getId()); + dos.writeShort(msg.getId()); byte[] data = msg.getData(); - buffer.putShort((short) data.length); - buffer.put(data); - output.write(rawBuffer, 0, buffer.position()); + dos.writeShort(data.length); + dos.write(data); break; default: - Ln.w("Unknown device message: " + msg.getType()); - break; + throw new ControlProtocolException("Unknown event type: " + type); } + dos.flush(); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index b1e6a9b9..d8cfd81f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.StringUtils; import android.os.Build; import android.os.HandlerThread; @@ -33,20 +34,20 @@ public final class UhidManager { private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; - private final HandlerThread thread = new HandlerThread("UHidManager"); private final MessageQueue queue; public UhidManager(DeviceMessageSender sender) { this.sender = sender; - thread.start(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + HandlerThread thread = new HandlerThread("UHidManager"); + thread.start(); queue = thread.getLooper().getQueue(); } else { queue = null; } } - public void open(int id, byte[] reportDesc) throws IOException { + public void open(int id, String name, byte[] reportDesc) throws IOException { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { @@ -56,7 +57,7 @@ public final class UhidManager { close(old); } - byte[] req = buildUhidCreate2Req(reportDesc); + byte[] req = buildUhidCreate2Req(name, reportDesc); Os.write(fd, req, 0, req.length); registerUhidListener(id, fd); @@ -95,6 +96,12 @@ public final class UhidManager { } } + private void unregisterUhidListener(FileDescriptor fd) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + queue.removeOnFileDescriptorEventListener(fd); + } + } + private static byte[] extractHidOutputData(ByteBuffer buffer) { /* * #define UHID_DATA_MAX 4096 @@ -140,7 +147,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) { /* * struct uhid_event { * uint32_t type; @@ -165,8 +172,14 @@ public final class UhidManager { byte[] empty = new byte[256]; ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); - buf.put("scrcpy".getBytes(StandardCharsets.US_ASCII)); - buf.put(empty, 0, 256 - "scrcpy".length()); + + String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name; + byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); + int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); + assert len <= 127; + buf.put(utf8Name, 0, len); + buf.put(empty, 0, 256 - len); + buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); buf.putInt(0); // vendor id @@ -199,9 +212,15 @@ public final class UhidManager { } public void close(int id) { - FileDescriptor fd = fds.get(id); - assert fd != null; - close(fd); + // Linux: Documentation/hid/uhid.rst + // If you close() the fd, the device is automatically unregistered and destroyed internally. + FileDescriptor fd = fds.remove(id); + if (fd != null) { + unregisterUhidListener(fd); + close(fd); + } else { + Ln.w("Closing unknown UHID device: " + id); + } } public void closeAll() { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 8fe0b227..a5f2d1e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -39,7 +39,7 @@ public class SurfaceEncoder implements AsyncProcessor { private final String encoderName; private final List codecOptions; private final int videoBitRate; - private final int maxFps; + private final float maxFps; private final boolean downsizeOnError; private boolean firstFrameSent; @@ -48,8 +48,8 @@ public class SurfaceEncoder implements AsyncProcessor { private Thread thread; private final AtomicBoolean stopped = new AtomicBoolean(); - public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, - boolean downsizeOnError) { + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List codecOptions, + String encoderName, boolean downsizeOnError) { this.capture = capture; this.streamer = streamer; this.videoBitRate = videoBitRate; @@ -225,7 +225,7 @@ public class SurfaceEncoder implements AsyncProcessor { } } - private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List codecOptions) { + private static MediaFormat createFormat(String videoMimeType, int bitRate, float maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, videoMimeType); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 1737730f..f29be2f4 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -18,8 +19,6 @@ public class ControlMessageReaderTest { @Test public void testParseKeycodeEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); @@ -29,23 +28,21 @@ public class ControlMessageReaderTest { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseTextEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); @@ -54,17 +51,18 @@ public class ControlMessageReaderTest { dos.write(text); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals("testé", event.getText()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseLongTextEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); @@ -74,17 +72,18 @@ public class ControlMessageReaderTest { dos.write(text); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseTouchEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT); @@ -100,12 +99,10 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(-42, event.getPointerId()); @@ -116,12 +113,12 @@ public class ControlMessageReaderTest { Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseScrollEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT); @@ -132,15 +129,12 @@ public class ControlMessageReaderTest { dos.writeShort(0); // 0.0f encoded as i16 dos.writeShort(0x8000); // -1.0f encoded as i16 dos.writeInt(1); - byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType()); Assert.assertEquals(260, event.getPosition().getPoint().getX()); Assert.assertEquals(1026, event.getPosition().getPoint().getY()); @@ -149,96 +143,96 @@ public class ControlMessageReaderTest { Assert.assertEquals(0f, event.getHScroll(), 0f); Assert.assertEquals(-1f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseBackOrScreenOnEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); dos.writeByte(KeyEvent.ACTION_UP); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseExpandNotificationPanelEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseExpandSettingsPanelEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseCollapsePanelsEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseGetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); dos.writeByte(ControlMessage.COPY_KEY_COPY); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseSetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); @@ -247,22 +241,22 @@ public class ControlMessageReaderTest { byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeInt(text.length); dos.write(text); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(0x0102030405060708L, event.getSequence()); Assert.assertEquals("testé", event.getText()); Assert.assertTrue(event.getPaste()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseBigSetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); @@ -278,78 +272,79 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(0x0807060504030201L, event.getSequence()); Assert.assertEquals(text, event.getText()); Assert.assertTrue(event.getPaste()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseSetScreenPowerMode() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); dos.writeByte(Device.POWER_MODE_NORMAL); - byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseRotateDevice() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseUhidCreate() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_CREATE); dos.writeShort(42); // id + dos.writeByte(3); // name size + dos.write("ABC".getBytes(StandardCharsets.US_ASCII)); byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; - dos.writeShort(data.length); // size + dos.writeShort(data.length); // report desc size dos.write(data); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); Assert.assertEquals(42, event.getId()); + Assert.assertEquals("ABC", event.getText()); Assert.assertArrayEquals(data, event.getData()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseUhidInput() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_INPUT); @@ -357,37 +352,55 @@ public class ControlMessageReaderTest { byte[] data = {1, 2, 3, 4, 5}; dos.writeShort(data.length); // size dos.write(data); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_INPUT, event.getType()); Assert.assertEquals(42, event.getId()); Assert.assertArrayEquals(data, event.getData()); + + Assert.assertEquals(-1, bis.read()); // EOS + } + + @Test + public void testParseUhidDestroy() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_DESTROY); + dos.writeShort(42); // id + byte[] packet = bos.toByteArray(); + + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); + Assert.assertEquals(ControlMessage.TYPE_UHID_DESTROY, event.getType()); + Assert.assertEquals(42, event.getId()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseOpenHardKeyboardSettings() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testMultiEvents() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); @@ -404,27 +417,29 @@ public class ControlMessageReaderTest { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(0, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - event = reader.next(); + event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(1, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testPartialEvents() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); @@ -438,31 +453,21 @@ public class ControlMessageReaderTest { dos.writeByte(MotionEvent.ACTION_DOWN); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); - ControlMessage event = reader.next(); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(4, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - event = reader.next(); - Assert.assertNull(event); // the event is not complete - - bos.reset(); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(5); // repeat - dos.writeInt(KeyEvent.META_CTRL_ON); - packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - - // the event is now complete - event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); - Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); - Assert.assertEquals(5, event.getRepeat()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + try { + event = reader.read(); + Assert.fail("Reader did not reach EOF"); + } catch (EOFException e) { + // expected + } } } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java index ff1a2fbc..4e4717fd 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java @@ -12,8 +12,6 @@ public class DeviceMessageWriterTest { @Test public void testSerializeClipboard() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - String text = "aéûoç"; byte[] data = text.getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -21,12 +19,13 @@ public class DeviceMessageWriterTest { dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); dos.writeInt(data.length); dos.write(data); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createClipboard(text); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createClipboard(text); + writer.write(msg); byte[] actual = bos.toByteArray(); @@ -35,18 +34,17 @@ public class DeviceMessageWriterTest { @Test public void testSerializeAckSetClipboard() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD); dos.writeLong(0x0102030405060708L); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); + writer.write(msg); byte[] actual = bos.toByteArray(); @@ -55,8 +53,6 @@ public class DeviceMessageWriterTest { @Test public void testSerializeUhidOutput() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_UHID_OUTPUT); @@ -64,12 +60,13 @@ public class DeviceMessageWriterTest { byte[] data = {1, 2, 3, 4, 5}; dos.writeShort(data.length); dos.write(data); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); + writer.write(msg); byte[] actual = bos.toByteArray();