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)
@@ -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();