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)
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) {