diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1c04da7f..576d4666 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,17 +7,25 @@ assignees: '' --- - - [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md). - - [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues). +_Please read the [prerequisites] to run scrcpy._ -**Environment** - - OS: [e.g. Debian, Windows, macOS...] - - scrcpy version: [e.g. 1.12.1] - - installation method: [e.g. manual build, apt, snap, brew, Windows release...] - - device model: - - Android version: [e.g. 10] +[prerequisites]: https://github.com/Genymobile/scrcpy#prerequisites + +_Also read the [FAQ] and check if your [issue][issues] already exists._ + +[FAQ]: https://github.com/Genymobile/scrcpy/blob/master/FAQ.md +[issues]: https://github.com/Genymobile/scrcpy/issues + +## Environment + + - **OS:** [e.g. Debian, Windows, macOS...] + - **Scrcpy version:** [e.g. 2.5] + - **Installation method:** [e.g. manual build, apt, snap, brew, Windows release...] + - **Device model:** + - **Android version:** [e.g. 14] + +## Describe the bug -**Describe the bug** A clear and concise description of what the bug is. On errors, please provide the output of the console (and `adb logcat` if relevant). diff --git a/README.md b/README.md index a672b327..3185652b 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.4) +# scrcpy (v2.5) scrcpy diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index e6b2c91a..b35ea5e4 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -25,7 +25,6 @@ _scrcpy() { -e --select-tcpip -f --fullscreen --force-adb-forward - --forward-all-clicks -h --help -K --keyboard= @@ -41,6 +40,7 @@ _scrcpy() { -M --max-fps= --mouse= + --mouse-bind= -n --no-control -N --no-playback --no-audio @@ -50,6 +50,7 @@ _scrcpy() { --no-downsize-on-error --no-key-repeat --no-mipmaps + --no-mouse-hover --no-power-on --no-video --no-video-playback diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index a23240ec..5afca977 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -32,10 +32,9 @@ 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]' - '--forward-all-clicks[Forward clicks to device]' {-h,--help}'[Print the help]' '-K[Use UHID keyboard (same as --keyboard=uhid)]' - '--keyboard[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' + '--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]' '--list-camera-sizes[List the valid camera capture sizes]' @@ -46,7 +45,8 @@ arguments=( {-m,--max-size=}'[Limit both the width and height of the video to value]' '-M[Use UHID mouse (same as --mouse=uhid)]' '--max-fps=[Limit the frame rate of screen capture]' - '--mouse[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' + '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' + '--mouse-bind=[Configure bindings of secondary clicks]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-playback}'[Disable video and audio playback]' '--no-audio[Disable audio forwarding]' @@ -56,6 +56,7 @@ arguments=( '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' + '--no-mouse-hover[Do not forward mouse hover events]' '--no-power-on[Do not power on the device on start]' '--no-video[Disable video forwarding]' '--no-video-playback[Disable video playback]' diff --git a/app/deps/adb.sh b/app/deps/adb.sh index e2408216..58a54659 100755 --- a/app/deps/adb.sh +++ b/app/deps/adb.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=34.0.5 +VERSION=35.0.0 FILENAME=platform-tools_r$VERSION-windows.zip PROJECT_DIR=platform-tools-$VERSION -SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571 +SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e cd "$SOURCES_DIR" diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 19fb2991..ef92d4a5 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=6.1.1 +VERSION=7.0.1 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=8684f4b00f94b85461884c3719382f1261f0d9eb3d59640a1f4ac0873616f968 +SHA256SUM=bce9eeb0f17ef8982390b1f37711a61b4290dc8c2a0c1a37b5857e85bfb0e4ff cd "$SOURCES_DIR" @@ -17,7 +17,6 @@ then else get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" - patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/ffmpeg-6.1-fix-build.patch fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 97fc3c72..26f0140b 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -5,9 +5,9 @@ cd "$DEPS_DIR" . common VERSION=1.0.27 -FILENAME=libusb-$VERSION.tar.bz2 +FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=ffaa41d741a8a3bee244ac8e54a72ea05bf2879663c098c82fc5757853441575 +SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de cd "$SOURCES_DIR" @@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/libusb-$VERSION.tar.bz2" "$FILENAME" "$SHA256SUM" + get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" fi @@ -33,6 +33,7 @@ else mkdir "$HOST" cd "$HOST" + "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh "$SOURCES_DIR/$PROJECT_DIR"/configure \ --prefix="$INSTALL_DIR/$HOST" \ --host="$HOST_TRIPLET" \ diff --git a/app/deps/patches/ffmpeg-6.1-fix-build.patch b/app/deps/patches/ffmpeg-6.1-fix-build.patch deleted file mode 100644 index ed4df48d..00000000 --- a/app/deps/patches/ffmpeg-6.1-fix-build.patch +++ /dev/null @@ -1,27 +0,0 @@ -From 03c80197afb324da38c9b70254231e3fdcfa68fc Mon Sep 17 00:00:00 2001 -From: Romain Vimont -Date: Sun, 12 Nov 2023 17:58:50 +0100 -Subject: [PATCH] Fix FFmpeg 6.1 build - -Build failed on tag n6.1 With --enable-decoder=av1 but without ---enable-muxer=av1. ---- - libavcodec/Makefile | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/libavcodec/Makefile b/libavcodec/Makefile -index 580a8d6b54..aff19b670c 100644 ---- a/libavcodec/Makefile -+++ b/libavcodec/Makefile -@@ -249,7 +249,7 @@ OBJS-$(CONFIG_ATRAC3PAL_DECODER) += atrac3plusdec.o atrac3plus.o \ - OBJS-$(CONFIG_ATRAC9_DECODER) += atrac9dec.o - OBJS-$(CONFIG_AURA_DECODER) += cyuv.o - OBJS-$(CONFIG_AURA2_DECODER) += aura.o --OBJS-$(CONFIG_AV1_DECODER) += av1dec.o -+OBJS-$(CONFIG_AV1_DECODER) += av1dec.o av1_parse.o - OBJS-$(CONFIG_AV1_CUVID_DECODER) += cuviddec.o - OBJS-$(CONFIG_AV1_MEDIACODEC_DECODER) += mediacodecdec.o - OBJS-$(CONFIG_AV1_MEDIACODEC_ENCODER) += mediacodecenc.o --- -2.42.0 - diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 36c7ab1c..589f93e5 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.28.5 +VERSION=2.30.4 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=9f0556e4a24ef5b267010038ad9e9948b62f236d5bcc4b22179f95ef62d84023 +SHA256SUM=dcc2c8c9c3e9e1a7c8d61d9522f1cba4e9b740feb560dcb15234030984610ee2 cd "$SOURCES_DIR" diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 059e91d4..717d9cb2 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.4" + VALUE "ProductVersion", "2.5" END END BLOCK "VarFileInfo" diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1e3c91b1..cf8dfa7f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -163,10 +163,6 @@ Start in fullscreen. .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. -.TP -.B \-\-forward\-all\-clicks -By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead. - .TP .B \-h, \-\-help Print this help. @@ -261,6 +257,23 @@ LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse bac Also see \fB\-\-keyboard\fR. +.TP +.BI "\-\-mouse\-bind " xxxx +Configure bindings of secondary clicks. + +The argument must be exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). + +Each character must be one of the following: + + - '+': forward the click to the device + - '-': ignore the click + - 'b': trigger shortcut BACK (or turn screen on if off) + - 'h': trigger shortcut HOME + - 's': trigger shortcut APP_SWITCH + - 'n': trigger shortcut "expand notification panel" + +Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID. + .TP .B \-n, \-\-no\-control @@ -304,6 +317,10 @@ Do not forward repeated key events when a key is held down. .B \-\-no\-mipmaps If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. +.TP +.B \-\-no\-mouse\-hover +Do not forward mouse hover (mouse motion without any clicks) events. + .TP .B \-\-no\-power\-on Do not power on the device on start. @@ -316,6 +333,10 @@ Disable video forwarding. .B \-\-no\-video\-playback Disable video playback on the computer. +.TP +.B \-\-no\-window +Disable scrcpy window. Implies --no-video-playback and --no-control. + .TP .BI "\-\-orientation " value Same as --display-orientation=value --record-orientation=value. @@ -420,9 +441,9 @@ Turn the device screen off immediately. .BI "\-\-shortcut\-mod " key\fR[+...]][,...] Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". -A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','. +Several shortcut modifiers can be specified, separated by ','. -For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper". +For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsuper". Default is "lalt,lsuper" (left-Alt or left-Super). @@ -577,6 +598,14 @@ Flip display horizontally .B MOD+Shift+Up, MOD+Shift+Down Flip display vertically +.TP +.B MOD+z +Pause or re-pause display + +.TP +.B MOD+Shift+z +Unpause display + .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index bd799c51..dac85bf9 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -194,7 +194,11 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // Still insufficient, drop old samples to make space skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining); assert(skipped_samples == remaining); + } + SDL_UnlockAudioDevice(ap->device); + + if (written < samples) { // Now there is enough space uint32_t w = sc_audiobuf_write(&ap->buf, swr_buf + TO_BYTES(written), @@ -202,8 +206,6 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, assert(w == remaining); (void) w; } - - SDL_UnlockAudioDevice(ap->device); } uint32_t underflow = 0; diff --git a/app/src/cli.c b/app/src/cli.c index daa041cf..08a4aa3f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -97,6 +97,9 @@ enum { OPT_MOUSE, OPT_HID_KEYBOARD_DEPRECATED, OPT_HID_MOUSE_DEPRECATED, + OPT_NO_WINDOW, + OPT_MOUSE_BIND, + OPT_NO_MOUSE_HOVER, }; struct sc_option { @@ -351,11 +354,9 @@ static const struct sc_option options[] = { "device.", }, { + // deprecated .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", - .text = "By default, right-click triggers BACK (or POWER on) and " - "middle-click triggers HOME. This option disables these " - "shortcuts and forwards the clicks to the device instead.", }, { .shortopt = 'h', @@ -489,6 +490,23 @@ static const struct sc_option options[] = { "control of the mouse back to the computer.\n" "Also see --keyboard.", }, + { + .longopt_id = OPT_MOUSE_BIND, + .longopt = "mouse-bind", + .argdesc = "xxxx", + .text = "Configure bindings of secondary clicks.\n" + "The argument must be exactly 4 characters, one for each " + "secondary click (in order: right click, middle click, 4th " + "click, 5th click).\n" + "Each character must be one of the following:\n" + " '+': forward the click to the device\n" + " '-': ignore the click\n" + " 'b': trigger shortcut BACK (or turn screen on if off)\n" + " 'h': trigger shortcut HOME\n" + " 's': trigger shortcut APP_SWITCH\n" + " 'n': trigger shortcut \"expand notification panel\"\n" + "Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID.", + }, { .shortopt = 'n', .longopt = "no-control", @@ -551,6 +569,12 @@ static const struct sc_option options[] = { "mipmaps are automatically generated to improve downscaling " "quality. This option disables the generation of mipmaps.", }, + { + .longopt_id = OPT_NO_MOUSE_HOVER, + .longopt = "no-mouse-hover", + .text = "Do not forward mouse hover (mouse motion without any clicks) " + "events.", + }, { .longopt_id = OPT_NO_POWER_ON, .longopt = "no-power-on", @@ -566,6 +590,12 @@ static const struct sc_option options[] = { .longopt = "no-video-playback", .text = "Disable video playback on the computer.", }, + { + .longopt_id = OPT_NO_WINDOW, + .longopt = "no-window", + .text = "Disable scrcpy window. Implies --no-video-playback and " + "--no-control.", + }, { .longopt_id = OPT_ORIENTATION, .longopt = "orientation", @@ -709,10 +739,10 @@ static const struct sc_option options[] = { .text = "Specify the modifiers to use for scrcpy shortcuts.\n" "Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", " "\"lsuper\" and \"rsuper\".\n" - "A shortcut can consist in several keys, separated by '+'. " - "Several shortcuts can be specified, separated by ','.\n" - "For example, to use either LCtrl+LAlt or LSuper for scrcpy " - "shortcuts, pass \"lctrl+lalt,lsuper\".\n" + "Several shortcut modifiers can be specified, separated by " + "','.\n" + "For example, to use either LCtrl or LSuper for scrcpy " + "shortcuts, pass \"lctrl,lsuper\".\n" "Default is \"lalt,lsuper\" (left-Alt or left-Super).", }, { @@ -900,6 +930,14 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" }, .text = "Flip display vertically", }, + { + .shortcuts = { "MOD+z" }, + .text = "Pause or re-pause display", + }, + { + .shortcuts = { "MOD+Shift+z" }, + .text = "Unpause display", + }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", @@ -1672,82 +1710,62 @@ parse_log_level(const char *s, enum sc_log_level *log_level) { return false; } -// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt") -// returns a bitwise-or of SC_SHORTCUT_MOD_* constants (or 0 on error) -static unsigned +static enum sc_shortcut_mod parse_shortcut_mods_item(const char *item, size_t len) { - unsigned mod = 0; - - for (;;) { - char *plus = strchr(item, '+'); - // strchr() does not consider the "len" parameter, to it could find an - // occurrence too far in the string (there is no strnchr()) - bool has_plus = plus && plus < item + len; - - assert(!has_plus || plus > item); - size_t key_len = has_plus ? (size_t) (plus - item) : len; - #define STREQ(literal, s, len) \ ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) - if (STREQ("lctrl", item, key_len)) { - mod |= SC_SHORTCUT_MOD_LCTRL; - } else if (STREQ("rctrl", item, key_len)) { - mod |= SC_SHORTCUT_MOD_RCTRL; - } else if (STREQ("lalt", item, key_len)) { - mod |= SC_SHORTCUT_MOD_LALT; - } else if (STREQ("ralt", item, key_len)) { - mod |= SC_SHORTCUT_MOD_RALT; - } else if (STREQ("lsuper", item, key_len)) { - mod |= SC_SHORTCUT_MOD_LSUPER; - } else if (STREQ("rsuper", item, key_len)) { - mod |= SC_SHORTCUT_MOD_RSUPER; - } else { - LOGE("Unknown modifier key: %.*s " - "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", - (int) key_len, item); - return 0; - } + if (STREQ("lctrl", item, len)) { + return SC_SHORTCUT_MOD_LCTRL; + } + if (STREQ("rctrl", item, len)) { + return SC_SHORTCUT_MOD_RCTRL; + } + if (STREQ("lalt", item, len)) { + return SC_SHORTCUT_MOD_LALT; + } + if (STREQ("ralt", item, len)) { + return SC_SHORTCUT_MOD_RALT; + } + if (STREQ("lsuper", item, len)) { + return SC_SHORTCUT_MOD_LSUPER; + } + if (STREQ("rsuper", item, len)) { + return SC_SHORTCUT_MOD_RSUPER; + } #undef STREQ - if (!has_plus) { - break; - } - - item = plus + 1; - assert(len >= key_len + 1); - len -= key_len + 1; + bool has_plus = strchr(item, '+'); + if (has_plus) { + LOGE("Shortcut mod combination with '+' is not supported anymore: " + "'%.*s' (see #4741)", (int) len, item); + return 0; } - return mod; + LOGE("Unknown modifier key: %.*s " + "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", + (int) len, item); + + return 0; } static bool -parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { - unsigned count = 0; - unsigned current = 0; +parse_shortcut_mods(const char *s, uint8_t *shortcut_mods) { + uint8_t mods = 0; - // LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper" + // A list of shortcut modifiers, for example "lctrl,rctrl,rsuper" for (;;) { char *comma = strchr(s, ','); - if (comma && count == SC_MAX_SHORTCUT_MODS - 1) { - assert(count < SC_MAX_SHORTCUT_MODS); - LOGW("Too many shortcut modifiers alternatives"); - return false; - } - assert(!comma || comma > s); size_t limit = comma ? (size_t) (comma - s) : strlen(s); - unsigned mod = parse_shortcut_mods_item(s, limit); + enum sc_shortcut_mod mod = parse_shortcut_mods_item(s, limit); if (!mod) { - LOGE("Invalid modifier keys: %.*s", (int) limit, s); return false; } - mods->data[current++] = mod; - ++count; + mods |= mod; if (!comma) { break; @@ -1756,7 +1774,7 @@ parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { s = comma + 1; } - mods->count = count; + *shortcut_mods = mods; return true; } @@ -1764,7 +1782,7 @@ parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { #ifdef SC_TEST // expose the function to unit-tests bool -sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { +sc_parse_shortcut_mods(const char *s, uint8_t *mods) { return parse_shortcut_mods(s, mods); } #endif @@ -2043,11 +2061,63 @@ parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { } LOGE("Unsupported pause on exit mode: %s " - "(expected true, false or if-error)", optarg); + "(expected true, false or if-error)", s); return false; } +static bool +parse_mouse_binding(char c, enum sc_mouse_binding *b) { + switch (c) { + case '+': + *b = SC_MOUSE_BINDING_CLICK; + return true; + case '-': + *b = SC_MOUSE_BINDING_DISABLED; + return true; + case 'b': + *b = SC_MOUSE_BINDING_BACK; + return true; + case 'h': + *b = SC_MOUSE_BINDING_HOME; + return true; + case 's': + *b = SC_MOUSE_BINDING_APP_SWITCH; + return true; + case 'n': + *b = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL; + return true; + default: + LOGE("Invalid mouse binding: '%c' " + "(expected '+', '-', 'b', 'h', 's' or 'n')", c); + return false; + } +} + +static bool +parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) { + if (strlen(s) != 4) { + LOGE("Invalid mouse bindings: '%s' (expected exactly 4 characters from " + "{'+', '-', 'b', 'h', 's', 'n'})", s); + return false; + } + + if (!parse_mouse_binding(s[0], &mb->right_click)) { + return false; + } + if (!parse_mouse_binding(s[1], &mb->middle_click)) { + return false; + } + if (!parse_mouse_binding(s[2], &mb->click4)) { + return false; + } + if (!parse_mouse_binding(s[3], &mb->click5)) { + return false; + } + + return true; +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -2130,6 +2200,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_MOUSE_BIND: + if (!parse_mouse_bindings(optarg, &opts->mouse_bindings)) { + return false; + } + break; + case OPT_NO_MOUSE_HOVER: + opts->mouse_hover = false; + break; case OPT_HID_MOUSE_DEPRECATED: LOGE("--hid-mouse has been removed, use --mouse=aoa or " "--mouse=uhid instead."); @@ -2327,7 +2405,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case OPT_FORWARD_ALL_CLICKS: - opts->forward_all_clicks = true; + LOGW("--forward-all-clicks is deprecated, " + "use --mouse-bind=++++ instead."); + opts->mouse_bindings = (struct sc_mouse_bindings) { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }; break; case OPT_LEGACY_PASTE: opts->legacy_paste = true; @@ -2478,6 +2563,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_CAMERA_HIGH_SPEED: opts->camera_high_speed = true; break; + case OPT_NO_WINDOW: + opts->window = false; + break; default: // getopt prints the error message on stderr return false; @@ -2515,6 +2603,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], v4l2 = !!opts->v4l2_device; #endif + if (!opts->window) { + // Without window, there cannot be any video playback or control + opts->video_playback = false; + opts->control = false; + } + if (!opts->video) { opts->video_playback = false; // Do not power on the device on start if video capture is disabled @@ -2536,8 +2630,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->audio = false; } - if (!opts->video && !opts->audio && !otg) { - LOGE("No video, no audio, no OTG: nothing to do"); + if (!opts->video && !opts->audio && !opts->control && !otg) { + LOGE("No video, no audio, no control, no OTG: nothing to do"); return false; } @@ -2548,9 +2642,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (opts->audio_playback && opts->audio_buffer == -1) { if (opts->audio_codec == SC_CODEC_FLAC) { - // Use 50 ms audio buffer by default, but use a higher value for FLAC, - // which is not low latency (the default encoder produces blocks of - // 4096 samples, which represent ~85.333ms). + // Use 50 ms audio buffer by default, but use a higher value for + // FLAC, which is not low latency (the default encoder produces + // blocks of 4096 samples, which represent ~85.333ms). LOGI("FLAC audio: audio buffer increased to 120 ms (use " "--audio-buffer to set a custom value)"); opts->audio_buffer = SC_TICK_FROM_MS(120); @@ -2561,6 +2655,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #ifdef HAVE_V4L2 if (v4l2) { + if (!opts->video) { + LOGE("V4L2 sink requires video capture, but --no-video was set."); + return false; + } + if (opts->lock_video_orientation == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " @@ -2580,16 +2679,57 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif - 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; + if (opts->control) { + 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; + } + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { + if (otg) { + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; + } else if (!opts->video_playback) { + LOGI("No video mirroring, mouse mode switched to UHID"); + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; + } else { + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; + } + } 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->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { - opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA - : SC_MOUSE_INPUT_MODE_SDK; + + // If mouse bindings are not explictly set, configure default bindings + if (opts->mouse_bindings.right_click == SC_MOUSE_BINDING_AUTO) { + assert(opts->mouse_bindings.middle_click == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.click4 == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.click5 == SC_MOUSE_BINDING_AUTO); + + // By default, forward all clicks only for UHID and AOA + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { + opts->mouse_bindings = (struct sc_mouse_bindings) { + .right_click = SC_MOUSE_BINDING_BACK, + .middle_click = SC_MOUSE_BINDING_HOME, + .click4 = SC_MOUSE_BINDING_APP_SWITCH, + .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, + }; + } else { + opts->mouse_bindings = (struct sc_mouse_bindings) { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }; + } } if (otg) { + if (!opts->control) { + LOGE("--no-control is not allowed in OTG mode"); + return false; + } + enum sc_keyboard_input_mode kmode = opts->keyboard_input_mode; if (kmode != SC_KEYBOARD_INPUT_MODE_AOA && kmode != SC_KEYBOARD_INPUT_MODE_DISABLED) { @@ -2628,6 +2768,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->mouse_input_mode != SC_MOUSE_INPUT_MODE_SDK + && !opts->mouse_hover) { + LOGE("--no-mouse-over is specific to --mouse=sdk"); + return false; + } + if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { LOGI("Tunnel host/port is set, " "--force-adb-forward automatically enabled."); @@ -2692,6 +2838,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->record_filename) { + if (!opts->video && !opts->audio) { + LOGE("Video and audio disabled, nothing to record"); + return false; + } + if (!opts->record_format) { opts->record_format = guess_record_format(opts->record_filename); if (!opts->record_format) { @@ -2798,6 +2949,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } # endif + if (opts->start_fps_counter && !opts->video_playback) { + LOGW("--print-fps has no effect without video playback"); + opts->start_fps_counter = false; + } + if (otg) { // OTG mode is compatible with only very few options. // Only report obvious errors. diff --git a/app/src/cli.h b/app/src/cli.h index 23d34fcd..6fd579a4 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -28,7 +28,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); #ifdef SC_TEST bool -sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods); +sc_parse_shortcut_mods(const char *s, uint8_t *shortcut_mods); #endif #endif diff --git a/app/src/controller.c b/app/src/controller.c index 499cfd3c..edd767eb 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -6,8 +6,19 @@ #define SC_CONTROL_MSG_QUEUE_MAX 64 +static void +sc_controller_receiver_on_error(struct sc_receiver *receiver, void *userdata) { + (void) receiver; + + struct sc_controller *controller = userdata; + // Forward the event to the controller listener + controller->cbs->on_error(controller, controller->cbs_userdata); +} + bool -sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { +sc_controller_init(struct sc_controller *controller, sc_socket control_socket, + const struct sc_controller_callbacks *cbs, + void *cbs_userdata) { sc_vecdeque_init(&controller->queue); bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); @@ -15,7 +26,12 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { return false; } - ok = sc_receiver_init(&controller->receiver, control_socket); + static const struct sc_receiver_callbacks receiver_cbs = { + .on_error = sc_controller_receiver_on_error, + }; + + ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs, + controller); if (!ok) { sc_vecdeque_destroy(&controller->queue); return false; @@ -39,6 +55,10 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { controller->control_socket = control_socket; controller->stopped = false; + assert(cbs && cbs->on_error); + controller->cbs = cbs; + controller->cbs_userdata = cbs_userdata; + return true; } @@ -125,10 +145,16 @@ run_controller(void *data) { sc_control_msg_destroy(&msg); if (!ok) { LOGD("Could not write msg to socket"); - break; + goto error; } } + return 0; + +error: + controller->cbs->on_error(controller, controller->cbs_userdata); + + return 1; // ignored } bool diff --git a/app/src/controller.h b/app/src/controller.h index 1e44427e..353d4d0d 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -22,10 +22,19 @@ struct sc_controller { bool stopped; struct sc_control_msg_queue queue; struct sc_receiver receiver; + + const struct sc_controller_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_controller_callbacks { + void (*on_error)(struct sc_controller *controller, void *userdata); }; bool -sc_controller_init(struct sc_controller *controller, sc_socket control_socket); +sc_controller_init(struct sc_controller *controller, sc_socket control_socket, + const struct sc_controller_callbacks *cbs, + void *cbs_userdata); void sc_controller_configure(struct sc_controller *controller, diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c27ea292..7223b553 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -278,7 +278,6 @@ run_demuxer(void *data) { finally_close_sinks: sc_packet_source_sinks_close(&demuxer->packet_source); finally_free_context: - // This also calls avcodec_close() internally avcodec_free_context(&codec_ctx); end: demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata); diff --git a/app/src/display.c b/app/src/display.c index c8df615d..9f5fb0c6 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -1,11 +1,34 @@ #include "display.h" #include +#include #include "util/log.h" +static bool +sc_display_init_novideo_icon(struct sc_display *display, + SDL_Surface *icon_novideo) { + assert(icon_novideo); + + if (SDL_RenderSetLogicalSize(display->renderer, + icon_novideo->w, icon_novideo->h)) { + LOGW("Could not set renderer logical size: %s", SDL_GetError()); + // don't fail + } + + display->texture = SDL_CreateTextureFromSurface(display->renderer, + icon_novideo); + if (!display->texture) { + LOGE("Could not create texture: %s", SDL_GetError()); + return false; + } + + return true; +} + bool -sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { +sc_display_init(struct sc_display *display, SDL_Window *window, + SDL_Surface *icon_novideo, bool mipmaps) { display->renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); if (!display->renderer) { @@ -65,6 +88,19 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { display->texture = NULL; display->pending.flags = 0; display->pending.frame = NULL; + display->has_frame = false; + + if (icon_novideo) { + // Without video, set a static scrcpy icon as window content + bool ok = sc_display_init_novideo_icon(display, icon_novideo); + if (!ok) { +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + SDL_GL_DeleteContext(display->gl_context); +#endif + SDL_DestroyRenderer(display->renderer); + return false; + } + } return true; } @@ -196,9 +232,25 @@ sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { return SC_DISPLAY_RESULT_OK; } +static SDL_YUV_CONVERSION_MODE +sc_display_to_sdl_color_range(enum AVColorRange color_range) { + return color_range == AVCOL_RANGE_JPEG ? SDL_YUV_CONVERSION_JPEG + : SDL_YUV_CONVERSION_AUTOMATIC; +} + static bool sc_display_update_texture_internal(struct sc_display *display, const AVFrame *frame) { + if (!display->has_frame) { + // First frame + display->has_frame = true; + + // Configure YUV color range conversion + SDL_YUV_CONVERSION_MODE sdl_color_range = + sc_display_to_sdl_color_range(frame->color_range); + SDL_SetYUVConversionMode(sdl_color_range); + } + int ret = SDL_UpdateYUVTexture(display->texture, NULL, frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], diff --git a/app/src/display.h b/app/src/display.h index 643ce73c..064bb7bf 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -33,6 +33,8 @@ struct sc_display { struct sc_size size; AVFrame *frame; } pending; + + bool has_frame; }; enum sc_display_result { @@ -42,7 +44,8 @@ enum sc_display_result { }; bool -sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps); +sc_display_init(struct sc_display *display, SDL_Window *window, + SDL_Surface *icon_novideo, bool mipmaps); void sc_display_destroy(struct sc_display *display); diff --git a/app/src/events.h b/app/src/events.h index 8bfa2582..3cf2b1dd 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -7,3 +7,4 @@ #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) diff --git a/app/src/icon.c b/app/src/icon.c index a9aad875..a76a85c9 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -78,7 +78,19 @@ decode_image(const char *path) { goto close_input; } - int stream = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); + +// In ffmpeg/doc/APIchanges: +// 2021-04-27 - 46dac8cf3d - lavf 59.0.100 - avformat.h +// av_find_best_stream now uses a const AVCodec ** parameter +// for the returned decoder. +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(59, 0, 100) + const AVCodec *codec; +#else + AVCodec *codec; +#endif + + int stream = + av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); if (stream < 0 ) { LOGE("Could not find best image stream"); goto close_input; @@ -86,12 +98,6 @@ decode_image(const char *path) { AVCodecParameters *params = ctx->streams[stream]->codecpar; - const AVCodec *codec = avcodec_find_decoder(params->codec_id); - if (!codec) { - LOGE("Could not find image decoder"); - goto close_input; - } - AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { LOG_OOM(); @@ -111,21 +117,21 @@ decode_image(const char *path) { AVFrame *frame = av_frame_alloc(); if (!frame) { LOG_OOM(); - goto close_codec; + goto free_codec_ctx; } AVPacket *packet = av_packet_alloc(); if (!packet) { LOG_OOM(); av_frame_free(&frame); - goto close_codec; + goto free_codec_ctx; } if (av_read_frame(ctx, packet) < 0) { LOGE("Could not read frame"); av_packet_free(&packet); av_frame_free(&frame); - goto close_codec; + goto free_codec_ctx; } int ret; @@ -133,22 +139,20 @@ decode_image(const char *path) { LOGE("Could not send icon packet: %d", ret); av_packet_free(&packet); av_frame_free(&frame); - goto close_codec; + goto free_codec_ctx; } if ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) { LOGE("Could not receive icon frame: %d", ret); av_packet_free(&packet); av_frame_free(&frame); - goto close_codec; + goto free_codec_ctx; } av_packet_free(&packet); result = frame; -close_codec: - avcodec_close(codec_ctx); free_codec_ctx: avcodec_free_context(&codec_ctx); close_input: diff --git a/app/src/input_events.h b/app/src/input_events.h index 5831ba0f..ed77bcb4 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -9,6 +9,7 @@ #include #include "coords.h" +#include "options.h" /* The representation of input events in scrcpy is very close to the SDL API, * for simplicity. @@ -437,15 +438,21 @@ sc_mouse_button_from_sdl(uint8_t button) { static inline uint8_t sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, - bool forward_all_clicks) { + const struct sc_mouse_bindings *mb) { assert(buttons_state < 0x100); // fits in uint8_t uint8_t mask = SC_MOUSE_BUTTON_LEFT; - if (forward_all_clicks) { - mask |= SC_MOUSE_BUTTON_RIGHT - | SC_MOUSE_BUTTON_MIDDLE - | SC_MOUSE_BUTTON_X1 - | SC_MOUSE_BUTTON_X2; + if (!mb || mb->right_click == SC_MOUSE_BINDING_CLICK) { + mask |= SC_MOUSE_BUTTON_RIGHT; + } + if (!mb || mb->middle_click == SC_MOUSE_BINDING_CLICK) { + mask |= SC_MOUSE_BUTTON_MIDDLE; + } + if (!mb || mb->click4 == SC_MOUSE_BINDING_CLICK) { + mask |= SC_MOUSE_BUTTON_X1; + } + if (!mb || mb->click5 == SC_MOUSE_BINDING_CLICK) { + mask |= SC_MOUSE_BUTTON_X2; } return buttons_state & mask; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f26c4164..43b10d2d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -10,7 +10,7 @@ #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) static inline uint16_t -to_sdl_mod(unsigned shortcut_mod) { +to_sdl_mod(uint8_t shortcut_mod) { uint16_t sdl_mod = 0; if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) { sdl_mod |= KMOD_LCTRL; @@ -38,15 +38,26 @@ is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { // keep only the relevant modifier keys sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; - assert(im->sdl_shortcut_mods.count); - assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS); - for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) { - if (im->sdl_shortcut_mods.data[i] == sdl_mod) { - return true; - } - } + // at least one shortcut mod pressed? + return sdl_mod & im->sdl_shortcut_mods; +} - return false; +static bool +is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) { + return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) + || (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) + || (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) + || (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) + || (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) + || (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); +} + +static inline bool +mouse_bindings_has_secondary_click(const struct sc_mouse_bindings *mb) { + return mb->right_click == SC_MOUSE_BINDING_CLICK + || mb->middle_click == SC_MOUSE_BINDING_CLICK + || mb->click4 == SC_MOUSE_BINDING_CLICK + || mb->click5 == SC_MOUSE_BINDING_CLICK; } void @@ -64,19 +75,13 @@ sc_input_manager_init(struct sc_input_manager *im, im->kp = params->kp; im->mp = params->mp; - im->forward_all_clicks = params->forward_all_clicks; + im->mouse_bindings = params->mouse_bindings; + im->has_secondary_click = + mouse_bindings_has_secondary_click(&im->mouse_bindings); im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; - const struct sc_shortcut_mods *shortcut_mods = params->shortcut_mods; - assert(shortcut_mods->count); - assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); - for (unsigned i = 0; i < shortcut_mods->count; ++i) { - uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]); - assert(sdl_mod); - im->sdl_shortcut_mods.data[i] = sdl_mod; - } - im->sdl_shortcut_mods.count = shortcut_mods->count; + im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods); im->vfinger_down = false; im->vfinger_invert_x = false; @@ -371,8 +376,8 @@ simulate_virtual_finger(struct sc_input_manager *im, msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.point = point; msg.inject_touch_event.pointer_id = - im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE - : POINTER_ID_VIRTUAL_FINGER; + im->has_secondary_click ? POINTER_ID_VIRTUAL_MOUSE + : POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; @@ -402,6 +407,8 @@ sc_input_manager_process_key(struct sc_input_manager *im, const SDL_KeyboardEvent *event) { // controller is NULL if --no-control is requested bool control = im->controller; + bool paused = im->screen->paused; + bool video = im->screen->video; SDL_Keycode keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; @@ -410,7 +417,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, bool shift = event->keysym.mod & KMOD_SHIFT; bool repeat = event->repeat; - bool smod = is_shortcut_mod(im, mod); + // Either the modifier includes a shortcut modifier, or the key + // press/release is a modifier key. + // 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); if (down && !repeat) { if (keycode == im->last_keycode && mod == im->last_mod) { @@ -422,68 +434,72 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } - // The shortcut modifier is pressed - if (smod) { + if (is_shortcut) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_home(im, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_back(im, action); } return; case SDLK_s: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_app_switch(im, action); } return; case SDLK_m: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_menu(im, action); } return; case SDLK_p: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_power(im, action); } return; case SDLK_o: - if (control && !repeat && down) { + if (control && !repeat && down && !paused) { enum sc_screen_power_mode mode = shift ? SC_SCREEN_POWER_MODE_NORMAL : SC_SCREEN_POWER_MODE_OFF; set_screen_power_mode(im, mode); } return; + case SDLK_z: + if (video && down && !repeat) { + sc_screen_set_paused(im->screen, !shift); + } + return; case SDLK_DOWN: if (shift) { - if (!repeat & down) { + if (video && !repeat && down) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } - } else if (im->kp) { + } else if (im->kp && !paused) { // forward repeated events action_volume_down(im, action); } return; case SDLK_UP: if (shift) { - if (!repeat & down) { + if (video && !repeat && down) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } - } else if (im->kp) { + } else if (im->kp && !paused) { // forward repeated events action_volume_up(im, action); } return; case SDLK_LEFT: - if (!repeat && down) { + if (video && !repeat && down) { if (shift) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_0); @@ -494,7 +510,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_RIGHT: - if (!repeat && down) { + if (video && !repeat && down) { if (shift) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_0); @@ -505,17 +521,17 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_c: - if (im->kp && !shift && !repeat && down) { + if (im->kp && !shift && !repeat && down && !paused) { get_device_clipboard(im, SC_COPY_KEY_COPY); } return; case SDLK_x: - if (im->kp && !shift && !repeat && down) { + if (im->kp && !shift && !repeat && down && !paused) { get_device_clipboard(im, SC_COPY_KEY_CUT); } return; case SDLK_v: - if (im->kp && !repeat && down) { + if (im->kp && !repeat && down && !paused) { if (shift || im->legacy_paste) { // inject the text as input events clipboard_paste(im); @@ -527,27 +543,27 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_f: - if (!shift && !repeat && down) { + if (video && !shift && !repeat && down) { sc_screen_switch_fullscreen(im->screen); } return; case SDLK_w: - if (!shift && !repeat && down) { + if (video && !shift && !repeat && down) { sc_screen_resize_to_fit(im->screen); } return; case SDLK_g: - if (!shift && !repeat && down) { + if (video && !shift && !repeat && down) { sc_screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: - if (!shift && !repeat && down) { + if (video && !shift && !repeat && down) { switch_fps_counter_state(im); } return; case SDLK_n: - if (control && !repeat && down) { + if (control && !repeat && down && !paused) { if (shift) { collapse_panels(im); } else if (im->key_repeat == 0) { @@ -558,12 +574,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_r: - if (control && !shift && !repeat && down) { + if (control && !shift && !repeat && down && !paused) { rotate_device(im); } return; case SDLK_k: - if (control && !shift && !repeat && down + if (control && !shift && !repeat && down && !paused && im->kp && im->kp->hid) { // Only if the current keyboard is hid open_hard_keyboard_settings(im); @@ -574,7 +590,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; } - if (!im->kp) { + if (!im->kp || paused) { return; } @@ -619,29 +635,39 @@ sc_input_manager_process_key(struct sc_input_manager *im, im->kp->ops->process_key(im->kp, &evt, ack_to_wait); } +static struct sc_position +sc_input_manager_get_position(struct sc_input_manager *im, int32_t x, + int32_t y) { + if (im->mp->relative_mode) { + // No absolute position + return (struct sc_position) { + .screen_size = {0, 0}, + .point = {0, 0}, + }; + } + + return (struct sc_position) { + .screen_size = im->screen->frame_size, + .point = sc_screen_convert_window_to_frame_coords(im->screen, x, y), + }; +} + static void sc_input_manager_process_mouse_motion(struct sc_input_manager *im, const SDL_MouseMotionEvent *event) { - if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; } struct sc_mouse_motion_event evt = { - .position = { - .screen_size = im->screen->frame_size, - .point = sc_screen_convert_window_to_frame_coords(im->screen, - event->x, - event->y), - }, - .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, + .position = sc_input_manager_get_position(im, event->x, event->y), + .pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE + : POINTER_ID_GENERIC_FINGER, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = - sc_mouse_buttons_state_from_sdl(event->state, - im->forward_all_clicks), + sc_mouse_buttons_state_from_sdl(event->state, &im->mouse_bindings), }; assert(im->mp->ops->process_mouse_motion); @@ -692,81 +718,109 @@ sc_input_manager_process_touch(struct sc_input_manager *im, im->mp->ops->process_touch(im->mp, &evt); } +static enum sc_mouse_binding +sc_input_manager_get_binding(const struct sc_mouse_bindings *bindings, + uint8_t sdl_button) { + switch (sdl_button) { + case SDL_BUTTON_LEFT: + return SC_MOUSE_BINDING_CLICK; + case SDL_BUTTON_RIGHT: + return bindings->right_click; + case SDL_BUTTON_MIDDLE: + return bindings->middle_click; + case SDL_BUTTON_X1: + return bindings->click4; + case SDL_BUTTON_X2: + return bindings->click5; + default: + return SC_MOUSE_BINDING_DISABLED; + } +} + static void sc_input_manager_process_mouse_button(struct sc_input_manager *im, const SDL_MouseButtonEvent *event) { - bool control = im->controller; - if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; } + bool control = im->controller; + bool paused = im->screen->paused; bool down = event->type == SDL_MOUSEBUTTONDOWN; - if (!im->forward_all_clicks) { - if (control) { - enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; + if (control && !paused) { + enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - if (im->kp && event->button == SDL_BUTTON_X1) { - action_app_switch(im, action); + enum sc_mouse_binding binding = + sc_input_manager_get_binding(&im->mouse_bindings, event->button); + assert(binding != SC_MOUSE_BINDING_AUTO); + switch (binding) { + case SC_MOUSE_BINDING_DISABLED: + // ignore click return; - } - if (event->button == SDL_BUTTON_X2 && down) { - if (event->clicks < 2) { - expand_notification_panel(im); - } else { - expand_settings_panel(im); + case SC_MOUSE_BINDING_BACK: + if (im->kp) { + press_back_or_turn_screen_on(im, action); } return; - } - if (im->kp && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(im, action); + case SC_MOUSE_BINDING_HOME: + if (im->kp) { + action_home(im, action); + } return; - } - if (im->kp && event->button == SDL_BUTTON_MIDDLE) { - action_home(im, action); + case SC_MOUSE_BINDING_APP_SWITCH: + if (im->kp) { + action_app_switch(im, action); + } return; - } - } - - // double-click on black borders resize to fit the device screen - if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { - int32_t x = event->x; - int32_t y = event->y; - sc_screen_hidpi_scale_coords(im->screen, &x, &y); - SDL_Rect *r = &im->screen->rect; - bool outside = x < r->x || x >= r->x + r->w - || y < r->y || y >= r->y + r->h; - if (outside) { + case SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL: if (down) { - sc_screen_resize_to_fit(im->screen); + if (event->clicks < 2) { + expand_notification_panel(im); + } else { + expand_settings_panel(im); + } } return; - } + default: + assert(binding == SC_MOUSE_BINDING_CLICK); + break; } - // otherwise, send the click event to the device } - if (!im->mp) { + // double-click on black borders resizes to fit the device screen + bool video = im->screen->video; + bool mouse_relative_mode = im->mp && im->mp->relative_mode; + if (video && !mouse_relative_mode && event->button == SDL_BUTTON_LEFT + && event->clicks == 2) { + int32_t x = event->x; + int32_t y = event->y; + sc_screen_hidpi_scale_coords(im->screen, &x, &y); + SDL_Rect *r = &im->screen->rect; + bool outside = x < r->x || x >= r->x + r->w + || y < r->y || y >= r->y + r->h; + if (outside) { + if (down) { + sc_screen_resize_to_fit(im->screen); + } + return; + } + } + + if (!im->mp || paused) { return; } uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); struct sc_mouse_click_event evt = { - .position = { - .screen_size = im->screen->frame_size, - .point = sc_screen_convert_window_to_frame_coords(im->screen, - event->x, - event->y), - }, + .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), - .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, - .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, - im->forward_all_clicks), + .pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE + : POINTER_ID_GENERIC_FINGER, + .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, + &im->mouse_bindings), }; assert(im->mp->ops->process_mouse_click); @@ -834,11 +888,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y); struct sc_mouse_scroll_event evt = { - .position = { - .screen_size = im->screen->frame_size, - .point = sc_screen_convert_window_to_frame_coords(im->screen, - mouse_x, mouse_y), - }, + .position = sc_input_manager_get_position(im, mouse_x, mouse_y), #if SDL_VERSION_ATLEAST(2, 0, 18) .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), @@ -846,8 +896,8 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, .hscroll = CLAMP(event->x, -1, 1), .vscroll = CLAMP(event->y, -1, 1), #endif - .buttons_state = - sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks), + .buttons_state = sc_mouse_buttons_state_from_sdl(buttons, + &im->mouse_bindings), }; im->mp->ops->process_mouse_scroll(im->mp, &evt); @@ -885,9 +935,10 @@ void sc_input_manager_handle_event(struct sc_input_manager *im, const SDL_Event *event) { bool control = im->controller; + bool paused = im->screen->paused; switch (event->type) { case SDL_TEXTINPUT: - if (!im->kp) { + if (!im->kp || paused) { break; } sc_input_manager_process_text_input(im, &event->text); @@ -899,13 +950,13 @@ sc_input_manager_handle_event(struct sc_input_manager *im, sc_input_manager_process_key(im, &event->key); break; case SDL_MOUSEMOTION: - if (!im->mp) { + if (!im->mp || paused) { break; } sc_input_manager_process_mouse_motion(im, &event->motion); break; case SDL_MOUSEWHEEL: - if (!im->mp) { + if (!im->mp || paused) { break; } sc_input_manager_process_mouse_wheel(im, &event->wheel); @@ -919,7 +970,7 @@ sc_input_manager_handle_event(struct sc_input_manager *im, case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: - if (!im->mp) { + if (!im->mp || paused) { break; } sc_input_manager_process_touch(im, &event->tfinger); diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 2ce11b03..03c42fe6 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -22,14 +22,12 @@ struct sc_input_manager { struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool forward_all_clicks; + struct sc_mouse_bindings mouse_bindings; + bool has_secondary_click; bool legacy_paste; bool clipboard_autosync; - struct { - unsigned data[SC_MAX_SHORTCUT_MODS]; - unsigned count; - } sdl_shortcut_mods; + uint16_t sdl_shortcut_mods; bool vfinger_down; bool vfinger_invert_x; @@ -52,10 +50,10 @@ struct sc_input_manager_params { struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool forward_all_clicks; + struct sc_mouse_bindings mouse_bindings; bool legacy_paste; bool clipboard_autosync; - const struct sc_shortcut_mods *shortcut_mods; + uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values }; void diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index 620fb52c..a7998972 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -58,17 +58,18 @@ convert_touch_action(enum sc_touch_action action) { static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { - if (!event->buttons_state) { + struct sc_mouse_sdk *m = DOWNCAST(mp); + + if (!m->mouse_hover && !event->buttons_state) { // Do not send motion events when no click is pressed return; } - struct sc_mouse_sdk *m = DOWNCAST(mp); - struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { - .action = AMOTION_EVENT_ACTION_MOVE, + .action = event->buttons_state ? AMOTION_EVENT_ACTION_MOVE + : AMOTION_EVENT_ACTION_HOVER_MOVE, .pointer_id = event->pointer_id, .position = event->position, .pressure = 1.f, @@ -145,8 +146,10 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, } void -sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller) { +sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller, + bool mouse_hover) { m->controller = controller; + m->mouse_hover = mouse_hover; static const struct sc_mouse_processor_ops ops = { .process_mouse_motion = sc_mouse_processor_process_mouse_motion, diff --git a/app/src/mouse_sdk.h b/app/src/mouse_sdk.h index 444a6ad5..142b89bb 100644 --- a/app/src/mouse_sdk.h +++ b/app/src/mouse_sdk.h @@ -13,9 +13,11 @@ struct sc_mouse_sdk { struct sc_mouse_processor mouse_processor; // mouse processor trait struct sc_controller *controller; + bool mouse_hover; }; void -sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller); +sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller, + bool mouse_hover); #endif diff --git a/app/src/options.c b/app/src/options.c index 7a885aa5..5556d1f9 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -23,6 +23,12 @@ 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, + .mouse_bindings = { + .right_click = SC_MOUSE_BINDING_AUTO, + .middle_click = SC_MOUSE_BINDING_AUTO, + .click4 = SC_MOUSE_BINDING_AUTO, + .click5 = SC_MOUSE_BINDING_AUTO, + }, .camera_facing = SC_CAMERA_FACING_ANY, .port_range = { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, @@ -30,10 +36,7 @@ const struct scrcpy_options scrcpy_options_default = { }, .tunnel_host = 0, .tunnel_port = 0, - .shortcut_mods = { - .data = {SC_SHORTCUT_MOD_LALT, SC_SHORTCUT_MOD_LSUPER}, - .count = 2, - }, + .shortcut_mods = SC_SHORTCUT_MOD_LALT | SC_SHORTCUT_MOD_LSUPER, .max_size = 0, .video_bit_rate = 0, .audio_bit_rate = 0, @@ -71,7 +74,6 @@ const struct scrcpy_options scrcpy_options_default = { .force_adb_forward = false, .disable_screensaver = false, .forward_key_repeat = true, - .forward_all_clicks = false, .legacy_paste = false, .power_off_on_close = false, .clipboard_autosync = true, @@ -89,6 +91,8 @@ const struct scrcpy_options scrcpy_options_default = { .kill_adb_on_close = false, .camera_high_speed = false, .list = 0, + .window = true, + .mouse_hover = true, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 5445e7c8..f840a989 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -155,6 +155,23 @@ enum sc_mouse_input_mode { SC_MOUSE_INPUT_MODE_AOA, }; +enum sc_mouse_binding { + SC_MOUSE_BINDING_AUTO, + SC_MOUSE_BINDING_DISABLED, + SC_MOUSE_BINDING_CLICK, + SC_MOUSE_BINDING_BACK, + SC_MOUSE_BINDING_HOME, + SC_MOUSE_BINDING_APP_SWITCH, + SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, +}; + +struct sc_mouse_bindings { + enum sc_mouse_binding right_click; + enum sc_mouse_binding middle_click; + enum sc_mouse_binding click4; + enum sc_mouse_binding click5; +}; + enum sc_key_inject_mode { // Inject special keys, letters and space as key events. // Inject numbers and punctuation as text events. @@ -169,8 +186,6 @@ enum sc_key_inject_mode { SC_KEY_INJECT_MODE_RAW, }; -#define SC_MAX_SHORTCUT_MODS 8 - enum sc_shortcut_mod { SC_SHORTCUT_MOD_LCTRL = 1 << 0, SC_SHORTCUT_MOD_RCTRL = 1 << 1, @@ -180,11 +195,6 @@ enum sc_shortcut_mod { SC_SHORTCUT_MOD_RSUPER = 1 << 5, }; -struct sc_shortcut_mods { - unsigned data[SC_MAX_SHORTCUT_MODS]; - unsigned count; -}; - struct sc_port_range { uint16_t first; uint16_t last; @@ -215,11 +225,12 @@ 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; + struct sc_mouse_bindings mouse_bindings; enum sc_camera_facing camera_facing; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; - struct sc_shortcut_mods shortcut_mods; + uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; @@ -257,7 +268,6 @@ struct scrcpy_options { bool force_adb_forward; bool disable_screensaver; bool forward_key_repeat; - bool forward_all_clicks; bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; @@ -279,6 +289,8 @@ struct scrcpy_options { #define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERA_SIZES 0x8 uint8_t list; + bool window; + bool mouse_hover; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/receiver.c b/app/src/receiver.c index f4ebd3f8..fb923ac4 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -10,7 +10,8 @@ #include "util/str.h" bool -sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) { +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, + const struct sc_receiver_callbacks *cbs, void *cbs_userdata) { bool ok = sc_mutex_init(&receiver->mutex); if (!ok) { return false; @@ -20,6 +21,10 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) { receiver->acksync = NULL; receiver->uhid_devices = NULL; + assert(cbs && cbs->on_error); + receiver->cbs = cbs; + receiver->cbs_userdata = cbs_userdata; + return true; } @@ -152,6 +157,8 @@ run_receiver(void *data) { } } + receiver->cbs->on_error(receiver, receiver->cbs_userdata); + return 0; } diff --git a/app/src/receiver.h b/app/src/receiver.h index ba84c0ab..ef83978f 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -19,10 +19,18 @@ struct sc_receiver { struct sc_acksync *acksync; struct sc_uhid_devices *uhid_devices; + + const struct sc_receiver_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_receiver_callbacks { + void (*on_error)(struct sc_receiver *receiver, void *userdata); }; bool -sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket); +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, + const struct sc_receiver_callbacks *cbs, void *cbs_userdata); void sc_receiver_destroy(struct sc_receiver *receiver); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f43af35e..5e78dbf3 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -174,6 +174,9 @@ event_loop(struct scrcpy *s) { case SC_EVENT_DEMUXER_ERROR: LOGE("Demuxer error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_CONTROLLER_ERROR: + LOGE("Controller error"); + return SCRCPY_EXIT_FAILURE; case SC_EVENT_RECORDER_ERROR: LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; @@ -265,6 +268,16 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, } } +static void +sc_controller_on_error(struct sc_controller *controller, void *userdata) { + // Note: this function may be called twice, once from the controller thread + // and once from the receiver thread + (void) controller; + (void) userdata; + + PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); +} + static void sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; @@ -408,7 +421,7 @@ scrcpy(struct scrcpy_options *options) { return SCRCPY_EXIT_FAILURE; } - if (options->video_playback) { + if (options->window) { // Set hints before starting the server thread to avoid race conditions // in SDL sdl_set_hints(options->render_driver); @@ -430,7 +443,7 @@ scrcpy(struct scrcpy_options *options) { assert(!options->video_playback || options->video); assert(!options->audio_playback || options->audio); - if (options->video_playback || + if (options->window || (options->control && options->clipboard_autosync)) { // Initialize the video subsystem even if --no-video or // --no-video-playback is passed so that clipboard synchronization @@ -553,7 +566,12 @@ scrcpy(struct scrcpy_options *options) { struct sc_mouse_processor *mp = NULL; if (options->control) { - if (!sc_controller_init(&s->controller, s->server.control_socket)) { + static const struct sc_controller_callbacks controller_cbs = { + .on_error = sc_controller_on_error, + }; + + if (!sc_controller_init(&s->controller, s->server.control_socket, + &controller_cbs, NULL)) { goto end; } controller_initialized = true; @@ -663,7 +681,8 @@ scrcpy(struct scrcpy_options *options) { } if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { - sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); + sc_mouse_sdk_init(&s->mouse_sdk, &s->controller, + options->mouse_hover); mp = &s->mouse_sdk.mouse_processor; } else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) { bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller); @@ -684,19 +703,20 @@ scrcpy(struct scrcpy_options *options) { // There is a controller if and only if control is enabled assert(options->control == !!controller); - if (options->video_playback) { + if (options->window) { const char *window_title = options->window_title ? options->window_title : info->device_name; struct sc_screen_params screen_params = { + .video = options->video_playback, .controller = controller, .fp = fp, .kp = kp, .mp = mp, - .forward_all_clicks = options->forward_all_clicks, + .mouse_bindings = options->mouse_bindings, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, - .shortcut_mods = &options->shortcut_mods, + .shortcut_mods = options->shortcut_mods, .window_title = window_title, .always_on_top = options->always_on_top, .window_x = options->window_x, @@ -710,12 +730,15 @@ scrcpy(struct scrcpy_options *options) { .start_fps_counter = options->start_fps_counter, }; - struct sc_frame_source *src = &s->video_decoder.frame_source; - if (options->display_buffer) { - sc_delay_buffer_init(&s->display_buffer, options->display_buffer, - true); - sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); - src = &s->display_buffer.frame_source; + struct sc_frame_source *src; + if (options->video_playback) { + src = &s->video_decoder.frame_source; + if (options->display_buffer) { + sc_delay_buffer_init(&s->display_buffer, + options->display_buffer, true); + sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); + src = &s->display_buffer.frame_source; + } } if (!sc_screen_init(&s->screen, &screen_params)) { @@ -723,7 +746,9 @@ scrcpy(struct scrcpy_options *options) { } screen_initialized = true; - sc_frame_source_add_sink(src, &s->screen.frame_sink); + if (options->video_playback) { + sc_frame_source_add_sink(src, &s->screen.frame_sink); + } } if (options->audio_playback) { @@ -805,9 +830,12 @@ scrcpy(struct scrcpy_options *options) { ret = event_loop(s); LOGD("quit..."); - // Close the window immediately on closing, because screen_destroy() may - // only be called once the video demuxer thread is joined (it may take time) - sc_screen_hide_window(&s->screen); + if (options->video_playback) { + // Close the window immediately on closing, because screen_destroy() + // may only be called once the video demuxer thread is joined (it may + // take time) + sc_screen_hide_window(&s->screen); + } end: if (timeout_started) { diff --git a/app/src/screen.c b/app/src/screen.c index 091001bc..55a06ab3 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -205,6 +205,8 @@ sc_screen_toggle_mouse_capture(struct sc_screen *screen) { static void sc_screen_update_content_rect(struct sc_screen *screen) { + assert(screen->video); + int dw; int dh; SDL_GL_GetDrawableSize(screen->window, &dw, &dh); @@ -246,6 +248,8 @@ sc_screen_update_content_rect(struct sc_screen *screen) { // changed, so that the content rectangle is recomputed static void sc_screen_render(struct sc_screen *screen, bool update_content_rect) { + assert(screen->video); + if (update_content_rect) { sc_screen_update_content_rect(screen); } @@ -255,6 +259,13 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { (void) res; // any error already logged } +static void +sc_screen_render_novideo(struct sc_screen *screen) { + enum sc_display_result res = + sc_display_render(&screen->display, NULL, SC_ORIENTATION_0); + (void) res; // any error already logged +} + #if defined(__APPLE__) || defined(__WINDOWS__) # define CONTINUOUS_RESIZING_WORKAROUND #endif @@ -268,6 +279,8 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { static int event_watcher(void *data, SDL_Event *event) { struct sc_screen *screen = data; + assert(screen->video); + if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { // In practice, it seems to always be called from the same thread in @@ -326,6 +339,7 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) { static bool sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_screen *screen = DOWNCAST(sink); + assert(screen->video); bool previous_skipped; bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped); @@ -362,6 +376,11 @@ sc_screen_init(struct sc_screen *screen, screen->maximized = false; screen->minimized = false; screen->mouse_capture_key_pressed = 0; + screen->paused = false; + screen->resume_frame = NULL; + screen->orientation = SC_ORIENTATION_0; + + screen->video = params->video; screen->req.x = params->window_x; screen->req.y = params->window_y; @@ -379,41 +398,75 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_frame_buffer; } - screen->orientation = params->orientation; - if (screen->orientation != SC_ORIENTATION_0) { - LOGI("Initial display orientation set to %s", - sc_orientation_get_name(screen->orientation)); + if (screen->video) { + screen->orientation = params->orientation; + if (screen->orientation != SC_ORIENTATION_0) { + LOGI("Initial display orientation set to %s", + sc_orientation_get_name(screen->orientation)); + } } - uint32_t window_flags = SDL_WINDOW_HIDDEN - | SDL_WINDOW_RESIZABLE - | SDL_WINDOW_ALLOW_HIGHDPI; + uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; } if (params->window_borderless) { window_flags |= SDL_WINDOW_BORDERLESS; } + if (params->video) { + // The window will be shown on first frame + window_flags |= SDL_WINDOW_HIDDEN + | SDL_WINDOW_RESIZABLE; + } + + const char *title = params->window_title; + assert(title); + + int x = SDL_WINDOWPOS_UNDEFINED; + int y = SDL_WINDOWPOS_UNDEFINED; + int width = 256; + int height = 256; + if (params->window_x != SC_WINDOW_POSITION_UNDEFINED) { + x = params->window_x; + } + if (params->window_y != SC_WINDOW_POSITION_UNDEFINED) { + y = params->window_y; + } + if (params->window_width) { + width = params->window_width; + } + if (params->window_height) { + height = params->window_height; + } // The window will be positioned and sized on first video frame - screen->window = - SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags); + screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags); if (!screen->window) { LOGE("Could not create window: %s", SDL_GetError()); goto error_destroy_fps_counter; } - ok = sc_display_init(&screen->display, screen->window, params->mipmaps); - if (!ok) { - goto error_destroy_window; - } - SDL_Surface *icon = scrcpy_icon_load(); if (icon) { SDL_SetWindowIcon(screen->window, icon); - scrcpy_icon_destroy(icon); - } else { + } else if (params->video) { + // just a warning LOGW("Could not load icon"); + } else { + // without video, the icon is used as window content, it must be present + LOGE("Could not load icon"); + goto error_destroy_fps_counter; + } + + SDL_Surface *icon_novideo = params->video ? NULL : icon; + bool mipmaps = params->video && params->mipmaps; + ok = sc_display_init(&screen->display, screen->window, icon_novideo, + mipmaps); + if (icon) { + scrcpy_icon_destroy(icon); + } + if (!ok) { + goto error_destroy_window; } screen->frame = av_frame_alloc(); @@ -428,7 +481,7 @@ sc_screen_init(struct sc_screen *screen, .screen = screen, .kp = params->kp, .mp = params->mp, - .forward_all_clicks = params->forward_all_clicks, + .mouse_bindings = params->mouse_bindings, .legacy_paste = params->legacy_paste, .clipboard_autosync = params->clipboard_autosync, .shortcut_mods = params->shortcut_mods, @@ -437,7 +490,9 @@ sc_screen_init(struct sc_screen *screen, sc_input_manager_init(&screen->im, &im_params); #ifdef CONTINUOUS_RESIZING_WORKAROUND - SDL_AddEventWatch(event_watcher, screen); + if (screen->video) { + SDL_AddEventWatch(event_watcher, screen); + } #endif static const struct sc_frame_sink_ops ops = { @@ -452,6 +507,11 @@ sc_screen_init(struct sc_screen *screen, screen->open = false; #endif + if (!screen->video && sc_screen_is_relative_mode(screen)) { + // Capture mouse immediately if video mirroring is disabled + sc_screen_set_mouse_capture(screen, true); + } + return true; error_destroy_display: @@ -522,6 +582,8 @@ sc_screen_destroy(struct sc_screen *screen) { static void resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, struct sc_size new_content_size) { + assert(screen->video); + struct sc_size window_size = get_window_size(screen); struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width @@ -535,6 +597,8 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, static void set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { + assert(screen->video); + if (!screen->fullscreen && !screen->maximized && !screen->minimized) { resize_for_content(screen, screen->content_size, new_content_size); } else if (!screen->resize_pending) { @@ -549,6 +613,8 @@ set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { static void apply_pending_resize(struct sc_screen *screen) { + assert(screen->video); + assert(!screen->fullscreen); assert(!screen->maximized); assert(!screen->minimized); @@ -562,6 +628,8 @@ apply_pending_resize(struct sc_screen *screen) { void sc_screen_set_orientation(struct sc_screen *screen, enum sc_orientation orientation) { + assert(screen->video); + if (orientation == screen->orientation) { return; } @@ -596,6 +664,8 @@ sc_screen_init_size(struct sc_screen *screen) { // recreate the texture and resize the window if the frame size has changed static enum sc_display_result prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { + assert(screen->video); + if (screen->frame_size.width == new_frame_size.width && screen->frame_size.height == new_frame_size.height) { return SC_DISPLAY_RESULT_OK; @@ -614,13 +684,12 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { } static bool -sc_screen_update_frame(struct sc_screen *screen) { - av_frame_unref(screen->frame); - sc_frame_buffer_consume(&screen->fb, screen->frame); - AVFrame *frame = screen->frame; +sc_screen_apply_frame(struct sc_screen *screen) { + assert(screen->video); sc_fps_counter_add_rendered_frame(&screen->fps_counter); + AVFrame *frame = screen->frame; struct sc_size new_frame_size = {frame->width, frame->height}; enum sc_display_result res = prepare_for_frame(screen, new_frame_size); if (res == SC_DISPLAY_RESULT_ERROR) { @@ -655,8 +724,62 @@ sc_screen_update_frame(struct sc_screen *screen) { return true; } +static bool +sc_screen_update_frame(struct sc_screen *screen) { + assert(screen->video); + + if (screen->paused) { + if (!screen->resume_frame) { + screen->resume_frame = av_frame_alloc(); + if (!screen->resume_frame) { + LOG_OOM(); + return false; + } + } else { + av_frame_unref(screen->resume_frame); + } + sc_frame_buffer_consume(&screen->fb, screen->resume_frame); + return true; + } + + av_frame_unref(screen->frame); + sc_frame_buffer_consume(&screen->fb, screen->frame); + return sc_screen_apply_frame(screen); +} + +void +sc_screen_set_paused(struct sc_screen *screen, bool paused) { + assert(screen->video); + + if (!paused && !screen->paused) { + // nothing to do + return; + } + + if (screen->paused && screen->resume_frame) { + // If display screen was paused, refresh the frame immediately, even if + // the new state is also paused. + av_frame_free(&screen->frame); + screen->frame = screen->resume_frame; + screen->resume_frame = NULL; + sc_screen_apply_frame(screen); + } + + if (!paused) { + LOGI("Display screen unpaused"); + } else if (!screen->paused) { + LOGI("Display screen paused"); + } else { + LOGI("Display screen re-paused"); + } + + screen->paused = paused; +} + void sc_screen_switch_fullscreen(struct sc_screen *screen) { + assert(screen->video); + uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); @@ -674,6 +797,8 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) { void sc_screen_resize_to_fit(struct sc_screen *screen) { + assert(screen->video); + if (screen->fullscreen || screen->maximized || screen->minimized) { return; } @@ -698,6 +823,8 @@ sc_screen_resize_to_fit(struct sc_screen *screen) { void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { + assert(screen->video); + if (screen->fullscreen || screen->minimized) { return; } @@ -741,6 +868,13 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { return true; } case SDL_WINDOWEVENT: + if (!screen->video + && event->window.event == SDL_WINDOWEVENT_EXPOSED) { + sc_screen_render_novideo(screen); + } + + // !video implies !has_frame + assert(screen->video || !screen->has_frame); if (!screen->has_frame) { // Do nothing return true; @@ -844,6 +978,8 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y) { + assert(screen->video); + enum sc_orientation orientation = screen->orientation; int32_t w = screen->content_size.width; diff --git a/app/src/screen.h b/app/src/screen.h index 46591be5..079d4fbb 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -26,6 +26,8 @@ struct sc_screen { bool open; // track the open/close state to assert correct behavior #endif + bool video; + struct sc_display display; struct sc_input_manager im; struct sc_frame_buffer fb; @@ -64,18 +66,23 @@ struct sc_screen { SDL_Keycode mouse_capture_key_pressed; AVFrame *frame; + + bool paused; + AVFrame *resume_frame; }; struct sc_screen_params { + bool video; + struct sc_controller *controller; struct sc_file_pusher *fp; struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool forward_all_clicks; + struct sc_mouse_bindings mouse_bindings; bool legacy_paste; bool clipboard_autosync; - const struct sc_shortcut_mods *shortcut_mods; + uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values const char *window_title; bool always_on_top; @@ -135,6 +142,10 @@ void sc_screen_set_orientation(struct sc_screen *screen, enum sc_orientation orientation); +// set the display pause state +void +sc_screen_set_paused(struct sc_screen *screen, bool paused); + // react to SDL events // If this function returns false, scrcpy must exit with an error. bool diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 6e9da09c..6ae33d86 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -176,6 +176,8 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, free(lpAttributeList); } + CloseHandle(pi.hThread); + // These handles are used by the child process, close them for this process if (pin) { CloseHandle(stdin_read_handle); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index e1d5cb01..33500e0c 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -169,7 +169,7 @@ sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen, // .position not used for HID events .xrel = event->xrel, .yrel = event->yrel, - .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, true), + .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, NULL), }; assert(mp->ops->process_mouse_motion); @@ -189,7 +189,7 @@ sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen, .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL), }; assert(mp->ops->process_mouse_click); @@ -209,7 +209,7 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, .hscroll = event->x, .vscroll = event->y, .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL), }; assert(mp->ops->process_mouse_scroll); diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c index 3597f7ee..3cc5cad1 100644 --- a/app/src/util/audiobuf.c +++ b/app/src/util/audiobuf.c @@ -46,6 +46,9 @@ sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) { uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size; + if (!can_read) { + return 0; + } if (samples_count > can_read) { samples_count = can_read; } @@ -86,6 +89,9 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; + if (!can_write) { + return 0; + } if (samples_count > can_write) { samples_count = can_write; } diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 3b3eb8d0..087e9af4 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -240,7 +240,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { vs->frame = av_frame_alloc(); if (!vs->frame) { LOG_OOM(); - goto error_avcodec_close; + goto error_avcodec_free_context; } vs->packet = av_packet_alloc(); @@ -268,8 +268,6 @@ error_av_packet_free: av_packet_free(&vs->packet); error_av_frame_free: av_frame_free(&vs->frame); -error_avcodec_close: - avcodec_close(vs->encoder_ctx); error_avcodec_free_context: avcodec_free_context(&vs->encoder_ctx); error_avio_close: @@ -297,7 +295,6 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { av_packet_free(&vs->packet); av_frame_free(&vs->frame); - avcodec_close(vs->encoder_ctx); avcodec_free_context(&vs->encoder_ctx); avio_close(vs->format_ctx->pb); avformat_free_context(vs->format_ctx); diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index f2a17272..cef8df3e 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -124,32 +124,22 @@ static void test_options2(void) { } static void test_parse_shortcut_mods(void) { - struct sc_shortcut_mods mods; + uint8_t mods; bool ok; ok = sc_parse_shortcut_mods("lctrl", &mods); assert(ok); - assert(mods.count == 1); - assert(mods.data[0] == SC_SHORTCUT_MOD_LCTRL); - - ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); - assert(ok); - assert(mods.count == 1); - assert(mods.data[0] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_LALT)); + assert(mods == SC_SHORTCUT_MOD_LCTRL); ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); assert(ok); - assert(mods.count == 2); - assert(mods.data[0] == SC_SHORTCUT_MOD_RCTRL); - assert(mods.data[1] == SC_SHORTCUT_MOD_LALT); + assert(mods == (SC_SHORTCUT_MOD_RCTRL | SC_SHORTCUT_MOD_LALT)); - ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods); + ok = sc_parse_shortcut_mods("lsuper,rsuper,lctrl", &mods); assert(ok); - assert(mods.count == 3); - assert(mods.data[0] == SC_SHORTCUT_MOD_LSUPER); - assert(mods.data[1] == (SC_SHORTCUT_MOD_RSUPER | SC_SHORTCUT_MOD_LALT)); - assert(mods.data[2] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_RCTRL | - SC_SHORTCUT_MOD_RALT)); + assert(mods == (SC_SHORTCUT_MOD_LSUPER + | SC_SHORTCUT_MOD_RSUPER + | SC_SHORTCUT_MOD_LCTRL)); ok = sc_parse_shortcut_mods("", &mods); assert(!ok); diff --git a/build.gradle b/build.gradle index b27befb6..f81f7d27 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.3' + classpath 'com.android.tools.build:gradle:8.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/doc/audio.md b/doc/audio.md index f1d4d8e7..0c0409a9 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -28,10 +28,17 @@ To disable only the audio playback, see [no playback](video.md#no-playback). ## Audio only -To play audio only, disable the video: +To play audio only, disable video and control: ```bash -scrcpy --no-video +scrcpy --no-video --no-control +``` + +To play audio without a window: + +```bash +# --no-video and --no-control are implied by --no-window +scrcpy --no-window # interrupt with Ctrl+C ``` diff --git a/doc/build.md b/doc/build.md index 01319a10..a35910f8 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.4`][direct-scrcpy-server] - SHA-256: `93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3` + - [`scrcpy-server-v2.5`][direct-scrcpy-server] + SHA-256: `1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/control.md b/doc/control.md index e9fd9e9b..34eb7a6a 100644 --- a/doc/control.md +++ b/doc/control.md @@ -15,6 +15,31 @@ scrcpy -n # short version Read [keyboard](keyboard.md) and [mouse](mouse.md). +## Control only + +To control the device without mirroring: + +```bash +scrcpy --no-video --no-audio +``` + +By default, mouse mode is switched to UHID if video mirroring is disabled (a +relative mouse mode is required). + +To also use a UHID keyboard, set it explicitly: + +```bash +scrcpy --no-video --no-audio --keyboard=uhid +scrcpy --no-video --no-audio -K # short version +``` + +To use AOA instead (over USB only): + +```bash +scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa +``` + + ## Copy-paste Any time the Android clipboard changes, it is automatically synchronized to the @@ -81,15 +106,6 @@ only inverts _x_. This only works for the default mouse mode (`--mouse=sdk`). -## Right-click and middle-click - -By default, right-click triggers BACK (or POWER on) and middle-click triggers -HOME. To disable these shortcuts and forward the clicks to the device instead: - -```bash -scrcpy --forward-all-clicks -``` - ## File drop ### Install APK diff --git a/doc/linux.md b/doc/linux.md index 68b4ee10..6bfe3454 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,7 +6,7 @@ Scrcpy is packaged in several distributions and package managers: - - Debian/Ubuntu: `apt install scrcpy` + - Debian/Ubuntu: ~~`apt install scrcpy`~~ _(obsolete version)_ - Arch Linux: `pacman -S scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Gentoo: `emerge scrcpy` diff --git a/doc/mouse.md b/doc/mouse.md index d0342954..1c62ddd0 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -18,6 +18,14 @@ Note that on some devices, an additional option must be enabled in developer options for this mouse mode to work. See [prerequisites](/README.md#prerequisites). +### Mouse hover + +By default, mouse hover (mouse motion without any clicks) events are forwarded +to the device. This can be disabled with: + +``` +scrcpy --no-mouse-hover +``` ## Physical mouse simulation @@ -68,3 +76,43 @@ debugging disabled (see [OTG](otg.md)). 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_). + + +## Mouse bindings + +By default, with SDK mouse, right-click triggers BACK (or POWER on) and +middle-click triggers HOME. In addition, the 4th click triggers APP_SWITCH and +the 5th click expands the notification panel. + +In AOA and UHID mouse modes, all clicks are forwarded by default. + +The shortcuts can be configured using `--mouse-bind=xxxx` for any mouse mode. +The argument must be exactly 4 characters, one for each secondary click: + +``` +--mouse-bind=xxxx + ^^^^ + |||| + ||| `- 5th click + || `-- 4th click + | `--- middle click + `---- right click +``` + +Each character must be one of the following: + + - `+`: forward the click to the device + - `-`: ignore the click + - `b`: trigger shortcut BACK (or turn screen on if off) + - `h`: trigger shortcut HOME + - `s`: trigger shortcut APP_SWITCH + - `n`: trigger shortcut "expand notification panel" + +For example: + +```bash +scrcpy --mouse-bind=bhsn # the default mode with SDK mouse +scrcpy --mouse-bind=++++ # forward all clicks (default for AOA/UHID) +scrcpy --mouse-bind=++bh # forward right and middle clicks, + # use 4th and 5th for BACK and HOME +``` diff --git a/doc/otg.md b/doc/otg.md index 3c7ed467..5f42ac9c 100644 --- a/doc/otg.md +++ b/doc/otg.md @@ -1,19 +1,21 @@ # OTG By default, _scrcpy_ injects input events at the Android API level. As an -alternative, when connected over USB, it is possible to send HID events, so that -scrcpy behaves as if it was a physical keyboard and/or mouse connected to the -Android device. +alternative, it is possible to send HID events, so that scrcpy behaves as if it +was a [physical keyboard] and/or a [physical mouse] connected to the Android +device (see [keyboard](keyboard.md) and [mouse](mouse.md)). -A special mode allows to control the device without mirroring, using AOA -[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa). Therefore, it is possible -to run _scrcpy_ with only physical keyboard and mouse simulation (HID), as if -the computer keyboard and mouse were plugged directly to the device via an OTG -cable. +[physical keyboard]: keyboard.md#physical-keyboard-simulation +[physical mouse]: physical-keyboard-simulation -In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. +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. -This is similar to `--keyboard=aoa --mouse=aoa`, but without mirroring. +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. To enable OTG mode: @@ -23,7 +25,7 @@ scrcpy --otg scrcpy --otg -s 0123456789abcdef ``` -It is possible to disable HID keyboard or HID mouse: +It is possible to disable keyboard or mouse: ```bash scrcpy --otg --keyboard=disabled @@ -35,3 +37,22 @@ It only works if the device is connected over USB. ## OTG issues on Windows See [FAQ](/FAQ.md#otg-issues-on-windows). + + +## Control only + +Note that the purpose of OTG is to control the device without USB debugging +(adb). + +If you want to solely control the device without mirroring while USB debugging +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 +``` + +One benefit of UHID is that it also works wirelessly. diff --git a/doc/recording.md b/doc/recording.md index 216542e9..f1a5a6e7 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -58,12 +58,10 @@ orientation](video.md#orientation). ## No playback -To disable playback while recording: +To disable playback and control while recording: ```bash -scrcpy --no-playback --record=file.mp4 -scrcpy -Nr file.mkv -# interrupt recording with Ctrl+C +scrcpy --no-playback --no-control --record=file.mp4 ``` It is also possible to disable video and audio playback separately: @@ -73,6 +71,13 @@ It is also possible to disable video and audio playback separately: scrcpy --record=file.mkv --no-audio-playback ``` +To also disable the window: + +```bash +scrcpy --no-playback --no-window --record=file.mp4 +# interrupt recording with Ctrl+C +``` + ## Time limit To limit the recording time: diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 8c402855..841ceaa6 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -13,8 +13,8 @@ It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, # use RCtrl for shortcuts scrcpy --shortcut-mod=rctrl -# use either LCtrl+LAlt or LSuper for shortcuts -scrcpy --shortcut-mod=lctrl+lalt,lsuper +# use either LCtrl or LSuper for shortcuts +scrcpy --shortcut-mod=lctrl,lsuper ``` _[Super] is typically the Windows or Cmd key._ @@ -28,6 +28,8 @@ _[Super] is typically the Windows or Cmd key._ | Rotate display right | MOD+ _(right)_ | Flip display horizontally | MOD+Shift+ _(left)_ \| MOD+Shift+ _(right)_ | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ + | Pause or re-pause display | MOD+z + | Unpause display | MOD+Shift+z | Resize window to 1:1 (pixel-perfect) | MOD+g | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ diff --git a/doc/window.md b/doc/window.md index b5b73921..b72c716c 100644 --- a/doc/window.md +++ b/doc/window.md @@ -1,5 +1,14 @@ # Window +## Disable window + +To disable window (may be useful for recording or for playing audio only): + +```bash +scrcpy --no-window --record=file.mp4 +# Ctrl+C to interrupt +``` + ## Title By default, the window title is the device model. It can be changed: diff --git a/doc/windows.md b/doc/windows.md index e3053188..139c3419 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.4.zip`][direct-win64] (64-bit) - SHA-256: `9dc56f21bfa455352ec0c58b40feaf2fb02d67372910a4235e298ece286ff3a9` - - [`scrcpy-win32-v2.4.zip`][direct-win32] (32-bit) - SHA-256: `cf92acc45eef37c6ee2db819f92e420ced3bc50f1348dd57f7d6ca1fc80f6116` + - [`scrcpy-win64-v2.5.zip`][direct-win64] (64-bit) + SHA-256: `345cf04a66a9144281dce72ca4e82adfd2c3092463196e586051df4c69e1507b` + - [`scrcpy-win32-v2.5.zip`][direct-win32] (32-bit) + SHA-256: `d56312a92471565fa4f3a6b94e8eb07717c4c90f2c0f05b03ba444e1001806ec` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win64-v2.4.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win32-v2.4.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win64-v2.5.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win32-v2.5.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 0be5675c..2bd6d7e6 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.4/scrcpy-server-v2.4 -PREBUILT_SERVER_SHA256=93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5 +PREBUILT_SERVER_SHA256=1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server diff --git a/meson.build b/meson.build index 22d0f4ef..1d11e574 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.4', + version: '2.5', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 6a1b09df..d17ffcb2 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20400 - versionName "2.4" + versionCode 20500 + versionName "2.5" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 7f7d7921..74bbd8ae 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.4 +SCRCPY_VERSION_NAME=2.5 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index a1003829..df3cf7c4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -127,6 +127,10 @@ public class CameraCapture extends SurfaceCapture { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class); + if (sizes == null) { + return null; + } + Stream stream = Arrays.stream(sizes); if (maxSize > 0) { stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize); diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index efa0672b..1ffb19d3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -118,12 +118,16 @@ public final class LogUtils { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); - for (android.util.Size size : sizes) { - builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); + if (sizes == null || sizes.length == 0) { + builder.append("\n (none)"); + } else { + for (android.util.Size size : sizes) { + builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); + } } android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes(); - if (highSpeedSizes.length > 0) { + if (highSpeedSizes != null && highSpeedSizes.length > 0) { builder.append("\n High speed capture (--camera-high-speed):"); for (android.util.Size size : highSpeedSizes) { Range[] highFpsRanges = configs.getHighSpeedVideoFpsRanges(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java index 95214188..090c96f0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -45,18 +45,18 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList } try { - display = createDisplay(); - setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); - Ln.d("Display: using SurfaceControl API"); - } catch (Exception surfaceControlException) { Rect videoRect = screenInfo.getVideoSize().toRect(); + virtualDisplay = ServiceManager.getDisplayManager() + .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface); + Ln.d("Display: using DisplayManager API"); + } catch (Exception displayManagerException) { try { - virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface); - Ln.d("Display: using DisplayManager API"); - } catch (Exception displayManagerException) { - Ln.e("Could not create display using SurfaceControl", surfaceControlException); + display = createDisplay(); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + Ln.d("Display: using SurfaceControl API"); + } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); + Ln.e("Could not create display using SurfaceControl", surfaceControlException); throw new AssertionError("Could not create display"); } } @@ -68,6 +68,11 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList device.setFoldListener(null); if (display != null) { SurfaceControl.destroyDisplay(display); + display = null; + } + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java index 28435c09..4a0fdf4e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; +import android.os.Build; import android.os.Looper; import android.os.SystemClock; import android.view.Surface; @@ -47,7 +48,7 @@ public class SurfaceEncoder implements AsyncProcessor { this.downsizeOnError = downsizeOnError; } - private void streamScreen() throws IOException, ConfigurationException { + private void streamCapture() throws IOException, ConfigurationException { Codec codec = streamer.getCodec(); MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); @@ -220,6 +221,9 @@ public class SurfaceEncoder implements AsyncProcessor { // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED); + } format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL); // display the very first frame, and recover from bad quality when no new frames format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs @@ -250,7 +254,7 @@ public class SurfaceEncoder implements AsyncProcessor { Looper.prepare(); try { - streamScreen(); + streamCapture(); } catch (ConfigurationException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index d9654b1b..44394ba9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -12,12 +12,15 @@ import java.lang.reflect.Method; public final class WindowManager { private final IInterface manager; private Method getRotationMethod; - private Method freezeRotationMethod; + private Method freezeDisplayRotationMethod; - private Method isRotationFrozenMethod; + private int freezeDisplayRotationMethodVersion; + private Method isDisplayRotationFrozenMethod; - private Method thawRotationMethod; + private int isDisplayRotationFrozenMethodVersion; + private Method thawDisplayRotationMethod; + private int thawDisplayRotationMethodVersion; static WindowManager create() { IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); @@ -43,50 +46,61 @@ public final class WindowManager { return getRotationMethod; } - private Method getFreezeRotationMethod() throws NoSuchMethodException { - if (freezeRotationMethod == null) { - freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); - } - return freezeRotationMethod; - } - - // New method added by this commit: - // private Method getFreezeDisplayRotationMethod() throws NoSuchMethodException { if (freezeDisplayRotationMethod == null) { - freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); + try { + // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: + // + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class, String.class); + freezeDisplayRotationMethodVersion = 0; + } catch (NoSuchMethodException e) { + try { + // New method added by this commit: + // + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); + freezeDisplayRotationMethodVersion = 1; + } catch (NoSuchMethodException e1) { + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); + freezeDisplayRotationMethodVersion = 2; + } + } } return freezeDisplayRotationMethod; } - private Method getIsRotationFrozenMethod() throws NoSuchMethodException { - if (isRotationFrozenMethod == null) { - isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); - } - return isRotationFrozenMethod; - } - - // New method added by this commit: - // private Method getIsDisplayRotationFrozenMethod() throws NoSuchMethodException { if (isDisplayRotationFrozenMethod == null) { - isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class); + try { + // New method added by this commit: + // + isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class); + isDisplayRotationFrozenMethodVersion = 0; + } catch (NoSuchMethodException e) { + isDisplayRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); + isDisplayRotationFrozenMethodVersion = 1; + } } return isDisplayRotationFrozenMethod; } - private Method getThawRotationMethod() throws NoSuchMethodException { - if (thawRotationMethod == null) { - thawRotationMethod = manager.getClass().getMethod("thawRotation"); - } - return thawRotationMethod; - } - - // New method added by this commit: - // private Method getThawDisplayRotationMethod() throws NoSuchMethodException { if (thawDisplayRotationMethod == null) { - thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); + try { + // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: + // + thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class, String.class); + thawDisplayRotationMethodVersion = 0; + } catch (NoSuchMethodException e) { + try { + // New method added by this commit: + // + thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); + thawDisplayRotationMethodVersion = 1; + } catch (NoSuchMethodException e1) { + thawDisplayRotationMethod = manager.getClass().getMethod("thawRotation"); + thawDisplayRotationMethodVersion = 2; + } + } } return thawDisplayRotationMethod; } @@ -103,16 +117,21 @@ public final class WindowManager { public void freezeRotation(int displayId, int rotation) { try { - try { - Method method = getFreezeDisplayRotationMethod(); - method.invoke(manager, displayId, rotation); - } catch (ReflectiveOperationException e) { - if (displayId == 0) { - Method method = getFreezeRotationMethod(); + Method method = getFreezeDisplayRotationMethod(); + switch (freezeDisplayRotationMethodVersion) { + case 0: + method.invoke(manager, displayId, rotation, "scrcpy#freezeRotation"); + break; + case 1: + method.invoke(manager, displayId, rotation); + break; + default: + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } method.invoke(manager, rotation); - } else { - Ln.e("Could not invoke method", e); - } + break; } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); @@ -121,17 +140,16 @@ public final class WindowManager { public boolean isRotationFrozen(int displayId) { try { - try { - Method method = getIsDisplayRotationFrozenMethod(); - return (boolean) method.invoke(manager, displayId); - } catch (ReflectiveOperationException e) { - if (displayId == 0) { - Method method = getIsRotationFrozenMethod(); + Method method = getIsDisplayRotationFrozenMethod(); + switch (isDisplayRotationFrozenMethodVersion) { + case 0: + return (boolean) method.invoke(manager, displayId); + default: + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return false; + } return (boolean) method.invoke(manager); - } else { - Ln.e("Could not invoke method", e); - return false; - } } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); @@ -141,16 +159,21 @@ public final class WindowManager { public void thawRotation(int displayId) { try { - try { - Method method = getThawDisplayRotationMethod(); - method.invoke(manager, displayId); - } catch (ReflectiveOperationException e) { - if (displayId == 0) { - Method method = getThawRotationMethod(); + Method method = getThawDisplayRotationMethod(); + switch (thawDisplayRotationMethodVersion) { + case 0: + method.invoke(manager, displayId, "scrcpy#thawRotation"); + break; + case 1: + method.invoke(manager, displayId); + break; + default: + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } method.invoke(manager); - } else { - Ln.e("Could not invoke method", e); - } + break; } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); @@ -166,6 +189,10 @@ public final class WindowManager { cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); } } catch (Exception e) {