From 7d5845196e6a7e919d0ec3b5fc53fc7dab8d3222 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Dec 2019 18:13:56 +0100 Subject: [PATCH 001/214] Fix memory leak on portable builds The function get_server_path() sometimes returned an owned string, sometimes a non-owned string. Always return an allocated (owned) string, and free it after usage. --- app/src/server.c | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index ff167aeb..31e09ae3 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -18,20 +18,31 @@ #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" -static const char * +static char * get_server_path(void) { const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); if (server_path_env) { - LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env); // if the envvar is set, use it - return server_path_env; + char *server_path = SDL_strdup(server_path_env); + if (!server_path) { + LOGE("Could not allocate memory"); + return NULL; + } + LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); + return server_path; } #ifndef PORTABLE LOGD("Using server: " DEFAULT_SERVER_PATH); + char *server_path = SDL_strdup(DEFAULT_SERVER_PATH); + if (!server_path) { + LOGE("Could not allocate memory"); + return NULL; + } // the absolute path is hardcoded - return DEFAULT_SERVER_PATH; + return server_path; #else + // use scrcpy-server in the same directory as the executable char *executable_path = get_executable_path(); if (!executable_path) { @@ -67,12 +78,17 @@ get_server_path(void) { static bool push_server(const char *serial) { - const char *server_path = get_server_path(); + char *server_path = get_server_path(); + if (!server_path) { + return false; + } if (!is_regular_file(server_path)) { LOGE("'%s' does not exist or is not a regular file\n", server_path); + SDL_free(server_path); return false; } process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); + SDL_free(server_path); return process_check_success(process, "adb push"); } From 78a320a763d747b04dcb0c6fed41a1edcc90101c Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Thu, 12 Dec 2019 20:26:58 +0800 Subject: [PATCH 002/214] Fix utf-8 char path in windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file 'E:\安安\scrcpy-win64-v1.12.1-1-g31bd950\scrcpy-server' exists, however, it will show msg as follow: INFO: scrcpy 1.12.1 stat: No such file or directory ERROR: 'E:\安安\scrcpy-win64-v1.12.1-1-g31bd950\scrcpy-server' does not exist or is not a regular file Press any key to continue... This patch fixes it. Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/command.c | 14 -------------- app/src/sys/unix/command.c | 12 ++++++++++++ app/src/sys/win/command.c | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/app/src/command.c b/app/src/command.c index 63afccb4..abaa223d 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -4,9 +4,6 @@ #include #include #include -#include -#include -#include #include "config.h" #include "common.h" @@ -205,14 +202,3 @@ process_check_success(process_t proc, const char *name) { } return true; } - -bool -is_regular_file(const char *path) { - struct stat path_stat; - int r = stat(path, &path_stat); - if (r) { - perror("stat"); - return false; - } - return S_ISREG(path_stat.st_mode); -} diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index fbcf2355..af5d4b2f 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -127,3 +128,14 @@ get_executable_path(void) { return NULL; #endif } + +bool +is_regular_file(const char *path) { + struct stat path_stat; + + if (stat(path, &path_stat)) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 55edaf8f..3846847e 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -4,6 +4,8 @@ #include "util/log.h" #include "util/str_util.h" +#include + static int build_cmd(char *cmd, size_t len, const char *const argv[]) { // Windows command-line parsing is WTF: @@ -90,3 +92,22 @@ get_executable_path(void) { buf[len] = '\0'; return utf8_from_wide_char(buf); } + +bool +is_regular_file(const char *path) { + wchar_t *wide_path = utf8_to_wide_char(path); + if (!wide_path) { + LOGC("Could not allocate wide char string"); + return false; + } + + struct _stat path_stat; + int r = _wstat(wide_path, &path_stat); + SDL_free(wide_path); + + if (r) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} From f9786e50346246139b67ab70d040443e3a80c16b Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 14 Dec 2019 14:34:49 +0800 Subject: [PATCH 003/214] Get env in windows correctly Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/server.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index 31e09ae3..54813ab8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -6,11 +6,13 @@ #include #include #include +#include #include "config.h" #include "command.h" #include "util/log.h" #include "util/net.h" +#include "util/str_util.h" #define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server" @@ -20,10 +22,18 @@ static char * get_server_path(void) { +#ifdef __WINDOWS__ + const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH"); +#else const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); +#endif if (server_path_env) { // if the envvar is set, use it +#ifdef __WINDOWS__ + char *server_path = utf8_from_wide_char(server_path_env); +#else char *server_path = SDL_strdup(server_path_env); +#endif if (!server_path) { LOGE("Could not allocate memory"); return NULL; From db6252e52b95af16976f6afa38028aca8c221036 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Dec 2019 21:55:43 +0100 Subject: [PATCH 004/214] Simplify net.c The platform-specific code for net.c was implemented in sys/*/net.c. But the differences are quite limited, so use ifdef-blocks in the single net.c instead. --- app/meson.build | 2 -- app/src/sys/unix/net.c | 21 --------------------- app/src/sys/win/net.c | 25 ------------------------- app/src/util/net.c | 30 ++++++++++++++++++++++++++++++ 4 files changed, 30 insertions(+), 48 deletions(-) delete mode 100644 app/src/sys/unix/net.c delete mode 100644 app/src/sys/win/net.c diff --git a/app/meson.build b/app/meson.build index 3bcb9bc1..62006ca1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -76,11 +76,9 @@ cc = meson.get_compiler('c') if host_machine.system() == 'windows' src += [ 'src/sys/win/command.c' ] - src += [ 'src/sys/win/net.c' ] dependencies += cc.find_library('ws2_32') else src += [ 'src/sys/unix/command.c' ] - src += [ 'src/sys/unix/net.c' ] endif conf = configuration_data() diff --git a/app/src/sys/unix/net.c b/app/src/sys/unix/net.c deleted file mode 100644 index d67a660f..00000000 --- a/app/src/sys/unix/net.c +++ /dev/null @@ -1,21 +0,0 @@ -#include "util/net.h" - -#include - -#include "config.h" - -bool -net_init(void) { - // do nothing - return true; -} - -void -net_cleanup(void) { - // do nothing -} - -bool -net_close(socket_t socket) { - return !close(socket); -} diff --git a/app/src/sys/win/net.c b/app/src/sys/win/net.c deleted file mode 100644 index aebce7fc..00000000 --- a/app/src/sys/win/net.c +++ /dev/null @@ -1,25 +0,0 @@ -#include "util/net.h" - -#include "config.h" -#include "util/log.h" - -bool -net_init(void) { - WSADATA wsa; - int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; - if (res < 0) { - LOGC("WSAStartup failed with error %d", res); - return false; - } - return true; -} - -void -net_cleanup(void) { - WSACleanup(); -} - -bool -net_close(socket_t socket) { - return !closesocket(socket); -} diff --git a/app/src/util/net.c b/app/src/util/net.c index bf4389dd..efce6fa9 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,6 +1,7 @@ #include "net.h" #include +#include #include "config.h" #include "log.h" @@ -115,3 +116,32 @@ bool net_shutdown(socket_t socket, int how) { return !shutdown(socket, how); } + +bool +net_init(void) { +#ifdef __WINDOWS__ + WSADATA wsa; + int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; + if (res < 0) { + LOGC("WSAStartup failed with error %d", res); + return false; + } +#endif + return true; +} + +void +net_cleanup(void) { +#ifdef __WINDOWS__ + WSACleanup(); +#endif +} + +bool +net_close(socket_t socket) { +#ifdef __WINDOWS__ + return !closesocket(socket); +#else + return !close(socket); +#endif +} From 83d48267a734e999fac0eccd93173b93019bf006 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 19 Dec 2019 11:49:50 +0100 Subject: [PATCH 005/214] Accept --max-fps before Android 10 KEY_MAX_FPS_TO_ENCODER existed privately before Android 10: --- README.md | 4 +++- app/scrcpy.1 | 2 +- app/src/cli.c | 4 ++-- .../main/java/com/genymobile/scrcpy/ScreenEncoder.java | 10 +++++----- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4d6d29fd..10e61bca 100644 --- a/README.md +++ b/README.md @@ -137,12 +137,14 @@ scrcpy -b 2M # short version #### Limit frame rate -On devices with Android >= 10, the capture frame rate can be limited: +The capture frame rate can be limited: ```bash scrcpy --max-fps 15 ``` +This is officially supported since Android 10, but may work on earlier versions. + #### Crop The device screen may be cropped to mirror only part of the screen. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 23b168ca..2600734b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -43,7 +43,7 @@ Print this help. .TP .BI "\-\-max\-fps " value -Limit the framerate of screen capture (only supported on devices with Android >= 10). +Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). .TP .BI "\-m, \-\-max\-size " value diff --git a/app/src/cli.c b/app/src/cli.c index d9e1013a..75025563 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -42,8 +42,8 @@ scrcpy_print_usage(const char *arg0) { " Print this help.\n" "\n" " --max-fps value\n" - " Limit the frame rate of screen capture (only supported on\n" - " devices with Android >= 10).\n" + " Limit the frame rate of screen capture (officially supported\n" + " since Android 10, but may work on earlier versions).\n" "\n" " -m, --max-size value\n" " Limit both the width and height of the video to value. The\n" diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c9a37f84..1c71eabd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -19,6 +19,7 @@ public class ScreenEncoder implements Device.RotationListener { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms + private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; private static final int NO_PTS = -1; @@ -150,11 +151,10 @@ public class ScreenEncoder implements Device.RotationListener { // 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 if (maxFps > 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps); - } else { - Ln.w("Max FPS is only supported since Android 10, the option has been ignored"); - } + // The key existed privately before Android 10: + // + // + format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); } return format; } From a8ceaf52846cddf61ea7da939db4d35dff969eb4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jan 2020 21:00:14 +0100 Subject: [PATCH 006/214] Fix include order --- app/src/sys/win/command.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 3846847e..105234b0 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -1,11 +1,11 @@ #include "command.h" +#include + #include "config.h" #include "util/log.h" #include "util/str_util.h" -#include - static int build_cmd(char *cmd, size_t len, const char *const argv[]) { // Windows command-line parsing is WTF: From d1a9a76cc6d738c455942d37138a3c365a423e88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 22:14:43 +0100 Subject: [PATCH 007/214] Reorder functions Move functions so that they can be called from enable_tunnel() (in the following commit). --- app/src/server.c | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 54813ab8..019b460a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -126,6 +126,20 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) { return process_check_success(process, "adb forward --remove"); } +static bool +disable_tunnel(struct server *server) { + if (server->tunnel_forward) { + return disable_tunnel_forward(server->serial, server->local_port); + } + return disable_tunnel_reverse(server->serial); +} + +static socket_t +listen_on_port(uint16_t port) { +#define IPV4_LOCALHOST 0x7F000001 + return net_listen(IPV4_LOCALHOST, port, 1); +} + static bool enable_tunnel(struct server *server) { if (enable_tunnel_reverse(server->serial, server->local_port)) { @@ -137,14 +151,6 @@ enable_tunnel(struct server *server) { return enable_tunnel_forward(server->serial, server->local_port); } -static bool -disable_tunnel(struct server *server) { - if (server->tunnel_forward) { - return disable_tunnel_forward(server->serial, server->local_port); - } - return disable_tunnel_reverse(server->serial); -} - static process_t execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; @@ -187,13 +193,6 @@ execute_server(struct server *server, const struct server_params *params) { return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } -#define IPV4_LOCALHOST 0x7F000001 - -static socket_t -listen_on_port(uint16_t port) { - return net_listen(IPV4_LOCALHOST, port, 1); -} - static socket_t connect_and_read_byte(uint16_t port) { socket_t socket = net_connect(IPV4_LOCALHOST, port); From ca0031cbde8cc4aaed07ddb7523841754c0423d5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 22:10:42 +0100 Subject: [PATCH 008/214] Refactor server tunnel initialization Start the server socket in enable_tunnel() directly. For the caller point of view, enabling the tunnel opens a port (either the server socket locally or the "adb forward" process). --- app/src/server.c | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 019b460a..0579a4b0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -143,9 +143,24 @@ listen_on_port(uint16_t port) { static bool enable_tunnel(struct server *server) { if (enable_tunnel_reverse(server->serial, server->local_port)) { + // At the application level, the device part is "the server" because it + // serves video stream and control. However, at the network level, the + // client listens and the server connects to the client. That way, the + // client can listen before starting the server app, so there is no + // need to try to connect until the server socket is listening on the + // device. + server->server_socket = listen_on_port(server->local_port); + if (server->server_socket == INVALID_SOCKET) { + LOGE("Could not listen on port %" PRIu16, server->local_port); + disable_tunnel(server); + return false; + } + return true; } + // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to + // "adb forward", so the app socket is the client LOGW("'adb reverse' failed, fallback to 'adb forward'"); server->tunnel_forward = true; return enable_tunnel_forward(server->serial, server->local_port); @@ -265,25 +280,6 @@ server_start(struct server *server, const char *serial, return false; } - // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to - // "adb forward", so the app socket is the client - if (!server->tunnel_forward) { - // At the application level, the device part is "the server" because it - // serves video stream and control. However, at the network level, the - // client listens and the server connects to the client. That way, the - // client can listen before starting the server app, so there is no - // need to try to connect until the server socket is listening on the - // device. - - server->server_socket = listen_on_port(params->local_port); - if (server->server_socket == INVALID_SOCKET) { - LOGE("Could not listen on port %" PRIu16, params->local_port); - disable_tunnel(server); - SDL_free(server->serial); - return false; - } - } - // server will connect to our server socket server->process = execute_server(server, params); From 2a3a9d4ea91f9b79b87387a4883af484e702c53a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 14:32:59 +0100 Subject: [PATCH 009/214] Add util function to parse a list of integers This will help parsing arguments like '1234:5678' into a list of integers. --- app/src/util/str_util.c | 29 +++++++++++++++++++++++ app/src/util/str_util.h | 5 ++++ app/tests/test_strutil.c | 50 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index 4d175407..ce0498a5 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -81,6 +81,35 @@ parse_integer(const char *s, long *out) { return true; } +size_t +parse_integers(const char *s, const char sep, size_t max_items, long *out) { + size_t count = 0; + char *endptr; + do { + errno = 0; + long value = strtol(s, &endptr, 0); + if (errno == ERANGE) { + return 0; + } + + if (endptr == s || (*endptr != sep && *endptr != '\0')) { + return 0; + } + + out[count++] = value; + if (*endptr == sep) { + if (count >= max_items) { + // max items already reached, could not accept a new item + return 0; + } + // parse the next token during the next iteration + s = endptr + 1; + } + } while (*endptr != '\0'); + + return count; +} + bool parse_integer_with_suffix(const char *s, long *out) { char *endptr; diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 8d9b990c..c7f26cdb 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -31,6 +31,11 @@ strquote(const char *src); bool parse_integer(const char *s, long *out); +// parse s as integers separated by sep (for example '1234:2000') +// returns the number of integers on success, 0 on failure +size_t +parse_integers(const char *s, const char sep, size_t max_items, long *out); + // parse s as an integer into value // like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as // suffix diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 200e0f63..a88bca0e 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -187,6 +187,55 @@ static void test_parse_integer(void) { assert(!ok); // out-of-range } +static void test_parse_integers(void) { + long values[5]; + + size_t count = parse_integers("1234", ':', 5, values); + assert(count == 1); + assert(values[0] == 1234); + + count = parse_integers("1234:5678", ':', 5, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == 5678); + + count = parse_integers("1234:5678", ':', 2, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == 5678); + + count = parse_integers("1234:-5678", ':', 2, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == -5678); + + count = parse_integers("1:2:3:4:5", ':', 5, values); + assert(count == 5); + assert(values[0] == 1); + assert(values[1] == 2); + assert(values[2] == 3); + assert(values[3] == 4); + assert(values[4] == 5); + + count = parse_integers("1234:5678", ':', 1, values); + assert(count == 0); // max_items == 1 + + count = parse_integers("1:2:3:4:5", ':', 3, values); + assert(count == 0); // max_items == 3 + + count = parse_integers(":1234", ':', 5, values); + assert(count == 0); // invalid + + count = parse_integers("1234:", ':', 5, values); + assert(count == 0); // invalid + + count = parse_integers("1234:", ':', 1, values); + assert(count == 0); // invalid, even when max_items == 1 + + count = parse_integers("1234::5678", ':', 5, values); + assert(count == 0); // invalid +} + static void test_parse_integer_with_suffix(void) { long value; bool ok = parse_integer_with_suffix("1234", &value); @@ -249,6 +298,7 @@ int main(void) { test_strquote(); test_utf8_truncate(); test_parse_integer(); + test_parse_integers(); test_parse_integer_with_suffix(); return 0; } From dc7fcf3c7a7a0e26c2447990206b798742e062c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 21:16:09 +0100 Subject: [PATCH 010/214] Accept port range Accept a range of ports to listen to, so that it does not fail if another instance of scrcpy is currently starting. The range can be passed via the command line: scrcpy -p 27183:27186 scrcpy -p 27183 # implicitly 27183:27183, as before The default is 27183:27199. Closes #951 --- app/meson.build | 5 +-- app/scrcpy.1 | 6 ++-- app/src/cli.c | 57 ++++++++++++++++++++++++------ app/src/common.h | 5 +++ app/src/scrcpy.c | 2 +- app/src/scrcpy.h | 8 +++-- app/src/server.c | 84 ++++++++++++++++++++++++++++++++++++++------ app/src/server.h | 26 ++++++++------ app/tests/test_cli.c | 5 +-- 9 files changed, 157 insertions(+), 41 deletions(-) diff --git a/app/meson.build b/app/meson.build index 62006ca1..171d6e35 100644 --- a/app/meson.build +++ b/app/meson.build @@ -96,9 +96,10 @@ conf.set_quoted('PREFIX', get_option('prefix')) # directory as the executable) conf.set('PORTABLE', get_option('portable')) -# the default client TCP port for the "adb reverse" tunnel +# the default client TCP port range for the "adb reverse" tunnel # overridden by option --port -conf.set('DEFAULT_LOCAL_PORT', '27183') +conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') +conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # the default max video size for both dimensions, in pixels # overridden by option --max-size diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2600734b..0c1bf5e4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -60,10 +60,10 @@ Disable device control (mirror the device in read\-only). Do not display device (only when screen recording is enabled). .TP -.BI "\-p, \-\-port " port -Set the TCP port the client listens on. +.BI "\-p, \-\-port " port[:port] +Set the TCP port (range) used by the client to listen. -Default is 27183. +Default is 27183:27199. .TP .B \-\-prefer\-text diff --git a/app/src/cli.c b/app/src/cli.c index 75025563..135dd5df 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -58,9 +58,9 @@ scrcpy_print_usage(const char *arg0) { " Do not display device (only when screen recording is\n" " enabled).\n" "\n" - " -p, --port port\n" - " Set the TCP port the client listens on.\n" - " Default is %d.\n" + " -p, --port port[:port]\n" + " Set the TCP port (range) used by the client to listen.\n" + " Default is %d:%d.\n" "\n" " --prefer-text\n" " Inject alpha characters and space as text events instead of\n" @@ -193,7 +193,7 @@ scrcpy_print_usage(const char *arg0) { arg0, DEFAULT_BIT_RATE, DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", - DEFAULT_LOCAL_PORT); + DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST); } static bool @@ -221,6 +221,27 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, return true; } +static size_t +parse_integers_arg(const char *s, size_t max_items, long *out, long min, + long max, const char *name) { + size_t count = parse_integers(s, ':', max_items, out); + if (!count) { + LOGE("Could not parse %s: %s", name, s); + return 0; + } + + for (size_t i = 0; i < count; ++i) { + long value = out[i]; + if (value < min || value > max) { + LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", + name, value, min, max); + return 0; + } + } + + return count; +} + static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; @@ -286,14 +307,30 @@ parse_window_dimension(const char *s, uint16_t *dimension) { } static bool -parse_port(const char *s, uint16_t *port) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); - if (!ok) { +parse_port_range(const char *s, struct port_range *port_range) { + long values[2]; + size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); + if (!count) { return false; } - *port = (uint16_t) value; + uint16_t v0 = (uint16_t) values[0]; + if (count == 1) { + port_range->first = v0; + port_range->last = v0; + return true; + } + + assert(count == 2); + uint16_t v1 = (uint16_t) values[1]; + if (v0 < v1) { + port_range->first = v0; + port_range->last = v1; + } else { + port_range->first = v1; + port_range->last = v0; + } + return true; } @@ -424,7 +461,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { opts->display = false; break; case 'p': - if (!parse_port(optarg, &opts->port)) { + if (!parse_port_range(optarg, &opts->port_range)) { return false; } break; diff --git a/app/src/common.h b/app/src/common.h index e5cbe953..4cbf1d74 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -27,4 +27,9 @@ struct position { struct point point; }; +struct port_range { + uint16_t first; + uint16_t last; +}; + #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17be1ed4..f315ca20 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -280,7 +280,7 @@ scrcpy(const struct scrcpy_options *options) { bool record = !!options->record_filename; struct server_params params = { .crop = options->crop, - .local_port = options->port, + .port_range = options->port_range, .max_size = options->max_size, .bit_rate = options->bit_rate, .max_fps = options->max_fps, diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 75de8717..9303a30c 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -5,6 +5,7 @@ #include #include "config.h" +#include "common.h" #include "input_manager.h" #include "recorder.h" @@ -15,7 +16,7 @@ struct scrcpy_options { const char *window_title; const char *push_target; enum recorder_format record_format; - uint16_t port; + struct port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; @@ -41,7 +42,10 @@ struct scrcpy_options { .window_title = NULL, \ .push_target = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \ - .port = DEFAULT_LOCAL_PORT, \ + .port_range = { \ + .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ + .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ + }, \ .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ diff --git a/app/src/server.c b/app/src/server.c index 0579a4b0..8392bd52 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -141,29 +141,91 @@ listen_on_port(uint16_t port) { } static bool -enable_tunnel(struct server *server) { - if (enable_tunnel_reverse(server->serial, server->local_port)) { +enable_tunnel_reverse_any_port(struct server *server, + struct port_range port_range) { + uint16_t port = port_range.first; + for (;;) { + if (!enable_tunnel_reverse(server->serial, port)) { + // the command itself failed, it will fail on any port + return false; + } + // At the application level, the device part is "the server" because it // serves video stream and control. However, at the network level, the // client listens and the server connects to the client. That way, the // client can listen before starting the server app, so there is no // need to try to connect until the server socket is listening on the // device. - server->server_socket = listen_on_port(server->local_port); - if (server->server_socket == INVALID_SOCKET) { - LOGE("Could not listen on port %" PRIu16, server->local_port); - disable_tunnel(server); - return false; + server->server_socket = listen_on_port(port); + if (server->server_socket != INVALID_SOCKET) { + // success + server->local_port = port; + return true; } + // failure, disable tunnel and try another port + if (!disable_tunnel_reverse(server->serial)) { + LOGW("Could not remove reverse tunnel on port %" PRIu16, port); + } + + // check before incrementing to avoid overflow on port 65535 + if (port < port_range.last) { + LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16, + port, port + 1); + port++; + continue; + } + + if (port_range.first == port_range.last) { + LOGE("Could not listen on port %" PRIu16, port_range.first); + } else { + LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16, + port_range.first, port_range.last); + } + return false; + } +} + +static bool +enable_tunnel_forward_any_port(struct server *server, + struct port_range port_range) { + server->tunnel_forward = true; + uint16_t port = port_range.first; + for (;;) { + if (enable_tunnel_forward(server->serial, port)) { + // success + server->local_port = port; + return true; + } + + if (port < port_range.last) { + LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, + port, port + 1); + port++; + continue; + } + + if (port_range.first == port_range.last) { + LOGE("Could not forward port %" PRIu16, port_range.first); + } else { + LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16, + port_range.first, port_range.last); + } + return false; + } +} + +static bool +enable_tunnel_any_port(struct server *server, struct port_range port_range) { + if (enable_tunnel_reverse_any_port(server, port_range)) { return true; } // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to // "adb forward", so the app socket is the client + LOGW("'adb reverse' failed, fallback to 'adb forward'"); - server->tunnel_forward = true; - return enable_tunnel_forward(server->serial, server->local_port); + return enable_tunnel_forward_any_port(server, port_range); } static process_t @@ -261,7 +323,7 @@ server_init(struct server *server) { bool server_start(struct server *server, const char *serial, const struct server_params *params) { - server->local_port = params->local_port; + server->port_range = params->port_range; if (serial) { server->serial = SDL_strdup(serial); @@ -275,7 +337,7 @@ server_start(struct server *server, const char *serial, return false; } - if (!enable_tunnel(server)) { + if (!enable_tunnel_any_port(server, params->port_range)) { SDL_free(server->serial); return false; } diff --git a/app/src/server.h b/app/src/server.h index 0cb1ab3a..8e3be81f 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -6,6 +6,7 @@ #include "config.h" #include "command.h" +#include "common.h" #include "util/net.h" struct server { @@ -14,25 +15,30 @@ struct server { socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; - uint16_t local_port; + struct port_range port_range; + uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" }; -#define SERVER_INITIALIZER { \ - .serial = NULL, \ - .process = PROCESS_NONE, \ - .server_socket = INVALID_SOCKET, \ - .video_socket = INVALID_SOCKET, \ +#define SERVER_INITIALIZER { \ + .serial = NULL, \ + .process = PROCESS_NONE, \ + .server_socket = INVALID_SOCKET, \ + .video_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \ - .local_port = 0, \ - .tunnel_enabled = false, \ - .tunnel_forward = false, \ + .port_range = { \ + .first = 0, \ + .last = 0, \ + }, \ + .local_port = 0, \ + .tunnel_enabled = false, \ + .tunnel_forward = false, \ } struct server_params { const char *crop; - uint16_t local_port; + struct port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 539c3c94..dfe95dba 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -50,7 +50,7 @@ static void test_options(void) { "--max-size", "1024", // "--no-control" is not compatible with "--turn-screen-off" // "--no-display" is not compatible with "--fulscreen" - "--port", "1234", + "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", "--record-format", "mkv", @@ -78,7 +78,8 @@ static void test_options(void) { assert(opts->fullscreen); assert(opts->max_fps == 30); assert(opts->max_size == 1024); - assert(opts->port == 1234); + assert(opts->port_range.first == 1234); + assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); assert(opts->record_format == RECORDER_FORMAT_MKV); From 96bd2c974d0fb9765dc4d2978688bb0bbd65cc9f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Jan 2020 15:49:25 +0100 Subject: [PATCH 011/214] Do not report workarounds errors Some workarounds are needed on some devices. But applying them may cause exceptions on other devices, where they are not necessary anyway. Do not report these errors in release builds. Closes #994 --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index b1b81903..fe5d8035 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -73,7 +73,7 @@ public final class Workarounds { mInitialApplicationField.set(activityThread, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error - Ln.w("Could not fill app info: " + throwable.getMessage()); + Ln.d("Could not fill app info: " + throwable.getMessage()); } } } From 4794ca8ae7515c7376b8e19aa8ec9c07546c965c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 25 Feb 2020 12:18:49 +0100 Subject: [PATCH 012/214] Use linear filtering Anisotropic filtering makes no sense for scrcpy use case. This (semantically) reverts 9e328ef98b0fc730036aa389f33649a31737d0e3. --- app/src/scrcpy.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f315ca20..a6c5c26e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -60,9 +60,9 @@ sdl_init_and_configure(bool display) { return true; } - // Use the best available scale quality - if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) { - LOGW("Could not enable bilinear filtering"); + // Linear filtering + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { + LOGW("Could not enable linear filtering"); } #ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH From ef56cc6ff74862e85c0af4746fa26ef391f6f3a9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Feb 2020 21:08:32 +0100 Subject: [PATCH 013/214] Retrieve screen info once The method getScreenInfo() is synchronized, and the result may change between calls. Call it once and store the result in a local variable. --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 1c71eabd..7edfacaf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -63,8 +63,9 @@ public class ScreenEncoder implements Device.RotationListener { do { MediaCodec codec = createCodec(); IBinder display = createDisplay(); - Rect contentRect = device.getScreenInfo().getContentRect(); - Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); + ScreenInfo screenInfo = device.getScreenInfo(); + Rect contentRect = screenInfo.getContentRect(); + Rect videoRect = screenInfo.getVideoSize().toRect(); setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); From 1982bc439b67829ec815b5e1bd13f1bf4e660f4b Mon Sep 17 00:00:00 2001 From: George Stamoulis Date: Sun, 16 Feb 2020 13:30:36 +0200 Subject: [PATCH 014/214] Add option to lock video orientation PR #1151 Signed-off-by: Romain Vimont --- README.md | 15 +++ app/meson.build | 6 + app/scrcpy.1 | 6 + app/src/cli.c | 110 +++++++++++------- app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 + app/src/server.c | 3 + app/src/server.h | 1 + app/tests/test_cli.c | 2 + .../java/com/genymobile/scrcpy/Device.java | 52 +++++++-- .../java/com/genymobile/scrcpy/Options.java | 9 ++ .../java/com/genymobile/scrcpy/Position.java | 13 +++ .../com/genymobile/scrcpy/ScreenEncoder.java | 28 +++-- .../com/genymobile/scrcpy/ScreenInfo.java | 28 +++-- .../java/com/genymobile/scrcpy/Server.java | 18 +-- 15 files changed, 221 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 07fe6cf3..998caae8 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,21 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) If `--max-size` is also specified, resizing is applied after cropping. +#### Lock video orientation + + +To lock the orientation of the mirroring: + +```bash +scrcpy --lock-video-orientation 0 # natural orientation +scrcpy --lock-video-orientation 1 # 90° counterclockwise +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° clockwise +``` + +This affects recording orientation. + + ### Recording It is possible to record the screen while mirroring: diff --git a/app/meson.build b/app/meson.build index 171d6e35..3df2f35c 100644 --- a/app/meson.build +++ b/app/meson.build @@ -105,6 +105,12 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # overridden by option --max-size conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited +# the default video orientation +# natural device orientation is 0 and each increment adds 90 degrees +# counterclockwise +# overridden by option --lock-video-orientation +conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked + # the default video bitrate, in bits/second # overridden by option --bit-rate conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a5d7404c..b3c57064 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -41,6 +41,12 @@ Start in fullscreen. .B \-h, \-\-help Print this help. +.TP +.BI "\-\-lock\-video\-orientation " value +Lock video orientation to \fIvalue\fR. Values are integers in the range [-1..3]. Natural device orientation is 0 and each increment adds 90 degrees counterclockwise. + +Default is -1 (unlocked). + .TP .BI "\-\-max\-fps " value Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). diff --git a/app/src/cli.c b/app/src/cli.c index 135dd5df..4b093c49 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -41,6 +41,12 @@ scrcpy_print_usage(const char *arg0) { " -h, --help\n" " Print this help.\n" "\n" + " --lock-video-orientation value\n" + " Lock video orientation to value. Values are integers in the\n" + " range [-1..3]. Natural device orientation is 0 and each\n" + " increment adds 90 degrees counterclockwise.\n" + " Default is %d%s.\n" + "\n" " --max-fps value\n" " Limit the frame rate of screen capture (officially supported\n" " since Android 10, but may work on earlier versions).\n" @@ -192,6 +198,7 @@ scrcpy_print_usage(const char *arg0) { "\n", arg0, DEFAULT_BIT_RATE, + DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)", DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST); } @@ -280,6 +287,19 @@ parse_max_fps(const char *s, uint16_t *max_fps) { return true; } +static bool +parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) { + long value; + bool ok = parse_integer_arg(s, &value, false, -1, 3, + "lock video orientation"); + if (!ok) { + return false; + } + + *lock_video_orientation = (int8_t) value; + return true; +} + static bool parse_window_position(const char *s, int16_t *position) { long value; @@ -364,51 +384,54 @@ guess_record_format(const char *filename) { return 0; } -#define OPT_RENDER_EXPIRED_FRAMES 1000 -#define OPT_WINDOW_TITLE 1001 -#define OPT_PUSH_TARGET 1002 -#define OPT_ALWAYS_ON_TOP 1003 -#define OPT_CROP 1004 -#define OPT_RECORD_FORMAT 1005 -#define OPT_PREFER_TEXT 1006 -#define OPT_WINDOW_X 1007 -#define OPT_WINDOW_Y 1008 -#define OPT_WINDOW_WIDTH 1009 -#define OPT_WINDOW_HEIGHT 1010 -#define OPT_WINDOW_BORDERLESS 1011 -#define OPT_MAX_FPS 1012 +#define OPT_RENDER_EXPIRED_FRAMES 1000 +#define OPT_WINDOW_TITLE 1001 +#define OPT_PUSH_TARGET 1002 +#define OPT_ALWAYS_ON_TOP 1003 +#define OPT_CROP 1004 +#define OPT_RECORD_FORMAT 1005 +#define OPT_PREFER_TEXT 1006 +#define OPT_WINDOW_X 1007 +#define OPT_WINDOW_Y 1008 +#define OPT_WINDOW_WIDTH 1009 +#define OPT_WINDOW_HEIGHT 1010 +#define OPT_WINDOW_BORDERLESS 1011 +#define OPT_MAX_FPS 1012 +#define OPT_LOCK_VIDEO_ORIENTATION 1013 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, - {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, OPT_CROP}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"max-fps", required_argument, NULL, OPT_MAX_FPS}, - {"max-size", required_argument, NULL, 'm'}, - {"no-control", no_argument, NULL, 'n'}, - {"no-display", no_argument, NULL, 'N'}, - {"port", required_argument, NULL, 'p'}, - {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, - {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, - {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, - {"serial", required_argument, NULL, 's'}, - {"show-touches", no_argument, NULL, 't'}, - {"turn-screen-off", no_argument, NULL, 'S'}, - {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, - {"version", no_argument, NULL, 'v'}, - {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, - {"window-x", required_argument, NULL, OPT_WINDOW_X}, - {"window-y", required_argument, NULL, OPT_WINDOW_Y}, - {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, - {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, - {"window-borderless", no_argument, NULL, - OPT_WINDOW_BORDERLESS}, - {NULL, 0, NULL, 0 }, + {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, + {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, OPT_CROP}, + {"fullscreen", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"lock-video-orientation", required_argument, NULL, + OPT_LOCK_VIDEO_ORIENTATION}, + {"max-fps", required_argument, NULL, OPT_MAX_FPS}, + {"max-size", required_argument, NULL, 'm'}, + {"no-control", no_argument, NULL, 'n'}, + {"no-display", no_argument, NULL, 'N'}, + {"port", required_argument, NULL, 'p'}, + {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, + {"record", required_argument, NULL, 'r'}, + {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, + {"render-expired-frames", no_argument, NULL, + OPT_RENDER_EXPIRED_FRAMES}, + {"serial", required_argument, NULL, 's'}, + {"show-touches", no_argument, NULL, 't'}, + {"turn-screen-off", no_argument, NULL, 'S'}, + {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, + {"version", no_argument, NULL, 'v'}, + {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, + {"window-x", required_argument, NULL, OPT_WINDOW_X}, + {"window-y", required_argument, NULL, OPT_WINDOW_Y}, + {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, + {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, + {"window-borderless", no_argument, NULL, + OPT_WINDOW_BORDERLESS}, + {NULL, 0, NULL, 0 }, }; struct scrcpy_options *opts = &args->opts; @@ -454,6 +477,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case OPT_LOCK_VIDEO_ORIENTATION: + if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { + return false; + } + break; case 'n': opts->control = false; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a6c5c26e..4d9ad88b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -284,6 +284,7 @@ scrcpy(const struct scrcpy_options *options) { .max_size = options->max_size, .bit_rate = options->bit_rate, .max_fps = options->max_fps, + .lock_video_orientation = options->lock_video_orientation, .control = options->control, }; if (!server_start(&server, options->serial, ¶ms)) { diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 9303a30c..e29298f2 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -20,6 +20,7 @@ struct scrcpy_options { uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; + int8_t lock_video_orientation; int16_t window_x; int16_t window_y; uint16_t window_width; @@ -49,6 +50,7 @@ struct scrcpy_options { .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ + .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ .window_x = -1, \ .window_y = -1, \ .window_width = 0, \ diff --git a/app/src/server.c b/app/src/server.c index 8392bd52..4b2c1866 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -233,9 +233,11 @@ execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; char max_fps_string[6]; + char lock_video_orientation_string[3]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(max_fps_string, "%"PRIu16, params->max_fps); + sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, @@ -251,6 +253,7 @@ execute_server(struct server *server, const struct server_params *params) { max_size_string, bit_rate_string, max_fps_string, + lock_video_orientation_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) diff --git a/app/src/server.h b/app/src/server.h index 8e3be81f..d84a5cc8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -42,6 +42,7 @@ struct server_params { uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; + int8_t lock_video_orientation; bool control; }; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index dfe95dba..c5d95633 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -48,6 +48,7 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", + "--lock-video-orientation", "2", // "--no-control" is not compatible with "--turn-screen-off" // "--no-display" is not compatible with "--fulscreen" "--port", "1234:1236", @@ -78,6 +79,7 @@ static void test_options(void) { assert(opts->fullscreen); assert(opts->max_fps == 30); assert(opts->max_size == 1024); + assert(opts->lock_video_orientation == 2); assert(opts->port_range.first == 1234); assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9448098a..7ccd0403 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -22,10 +22,12 @@ public final class Device { private final ServiceManager serviceManager = new ServiceManager(); + private final int lockedVideoOrientation; private ScreenInfo screenInfo; private RotationListener rotationListener; public Device(Options options) { + lockedVideoOrientation = options.getLockedVideoOrientation(); screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); registerRotationWatcher(new IRotationWatcher.Stub() { @Override @@ -48,11 +50,11 @@ public final class Device { private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); - boolean rotated = (displayInfo.getRotation() & 1) != 0; + int rotation = displayInfo.getRotation(); Size deviceSize = displayInfo.getSize(); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); if (crop != null) { - if (rotated) { + if (rotation % 2 != 0) { // 180s preserve dimensions // the crop (provided by the user) is expressed in the natural orientation crop = flipRect(crop); } @@ -64,7 +66,7 @@ public final class Device { } Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); - return new ScreenInfo(contentRect, videoSize, rotated); + return new ScreenInfo(contentRect, videoSize, rotation); } private static String formatCrop(Rect rect) { @@ -99,22 +101,56 @@ public final class Device { return new Size(w, h); } + /** + * Return the rotation to apply to the device rotation to get the requested locked video orientation + * + * @param deviceRotation the device rotation + * @return the rotation offset + */ + public int getVideoRotation(int deviceRotation) { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (deviceRotation + 4 - lockedVideoOrientation) % 4; + } + + /** + * Return the rotation to apply to the requested locked video orientation to get the device rotation + * + * @param deviceRotation the device rotation + * @return the (reverse) rotation offset + */ + private int getReverseVideoRotation(int deviceRotation) { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (lockedVideoOrientation + 4 - deviceRotation) % 4; + } + public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") ScreenInfo screenInfo = getScreenInfo(); // read with synchronization Size videoSize = screenInfo.getVideoSize(); - Size clientVideoSize = position.getScreenSize(); + + int deviceRotation = screenInfo.getRotation(); + int reverseVideoRotation = getReverseVideoRotation(deviceRotation); + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(reverseVideoRotation); + + Size clientVideoSize = devicePosition.getScreenSize(); if (!videoSize.equals(clientVideoSize)) { // The client sends a click relative to a video with wrong dimensions, // the device may have been rotated since the event was generated, so ignore the event return null; } Rect contentRect = screenInfo.getContentRect(); - Point point = position.getPoint(); - int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); - int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); - return new Point(scaledX, scaledY); + Point point = devicePosition.getPoint(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); + return new Point(convertedX, convertedY); } public static String getDeviceName() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 5b993f30..d9a29452 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -6,6 +6,7 @@ public class Options { private int maxSize; private int bitRate; private int maxFps; + private int lockedVideoOrientation; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly @@ -35,6 +36,14 @@ public class Options { this.maxFps = maxFps; } + public int getLockedVideoOrientation() { + return lockedVideoOrientation; + } + + public void setLockedVideoOrientation(int lockedVideoOrientation) { + this.lockedVideoOrientation = lockedVideoOrientation; + } + public boolean isTunnelForward() { return tunnelForward; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index b46d2f73..e9b6d8a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -23,6 +23,19 @@ public class Position { return screenSize; } + public Position rotate(int rotation) { + switch (rotation) { + case 1: + return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate()); + case 2: + return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize); + case 3: + return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate()); + default: + return this; + } + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 7edfacaf..3e9772ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -28,19 +28,21 @@ public class ScreenEncoder implements Device.RotationListener { private int bitRate; private int maxFps; + private int lockedVideoOrientation; private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; + this.lockedVideoOrientation = lockedVideoOrientation; this.iFrameInterval = iFrameInterval; } - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { - this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation) { + this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -66,10 +68,11 @@ public class ScreenEncoder implements Device.RotationListener { ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); Rect videoRect = screenInfo.getVideoSize().toRect(); - setSize(format, videoRect.width(), videoRect.height()); + int videoRotation = device.getVideoRotation(screenInfo.getRotation()); + setSize(format, videoRotation, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, contentRect, videoRect); + setDisplaySurface(display, surface, videoRotation, contentRect, videoRect); codec.start(); try { alive = encode(codec, fd); @@ -168,16 +171,21 @@ public class ScreenEncoder implements Device.RotationListener { codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); } - private static void setSize(MediaFormat format, int width, int height) { - format.setInteger(MediaFormat.KEY_WIDTH, width); - format.setInteger(MediaFormat.KEY_HEIGHT, height); + private static void setSize(MediaFormat format, int orientation, int width, int height) { + if (orientation % 2 == 0) { + format.setInteger(MediaFormat.KEY_WIDTH, width); + format.setInteger(MediaFormat.KEY_HEIGHT, height); + return; + } + format.setInteger(MediaFormat.KEY_WIDTH, height); + format.setInteger(MediaFormat.KEY_HEIGHT, width); } - private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) { + private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); + SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); SurfaceControl.setDisplayLayerStack(display, 0); } finally { SurfaceControl.closeTransaction(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index f2fce1d6..0f01615e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -5,12 +5,12 @@ import android.graphics.Rect; public final class ScreenInfo { private final Rect contentRect; // device size, possibly cropped private final Size videoSize; - private final boolean rotated; + private final int rotation; - public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { + public ScreenInfo(Rect contentRect, Size videoSize, int rotation) { this.contentRect = contentRect; this.videoSize = videoSize; - this.rotated = rotated; + this.rotation = rotation; } public Rect getContentRect() { @@ -21,11 +21,25 @@ public final class ScreenInfo { return videoSize; } - public ScreenInfo withRotation(int rotation) { - boolean newRotated = (rotation & 1) != 0; - if (rotated == newRotated) { + public int getRotation() { + return rotation; + } + + public ScreenInfo withRotation(int newRotation) { + if (newRotation == rotation) { return this; } - return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); + // true if changed between portrait and landscape + boolean orientationChanged = (rotation + newRotation) % 2 != 0; + Rect newContentRect; + Size newVideoSize; + if (orientationChanged) { + newContentRect = Device.flipRect(contentRect); + newVideoSize = videoSize.rotate(); + } else { + newContentRect = contentRect; + newVideoSize = videoSize; + } + return new ScreenInfo(newContentRect, newVideoSize, newRotation); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 56b738fb..2b0d32a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -19,7 +19,8 @@ public final class Server { final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), + options.getLockedVideoOrientation()); if (options.getControl()) { Controller controller = new Controller(device, connection); @@ -79,8 +80,8 @@ public final class Server { "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); } - if (args.length != 8) { - throw new IllegalArgumentException("Expecting 8 parameters"); + if (args.length != 9) { + throw new IllegalArgumentException("Expecting 9 parameters"); } Options options = new Options(); @@ -94,17 +95,20 @@ public final class Server { int maxFps = Integer.parseInt(args[3]); options.setMaxFps(maxFps); + int lockedVideoOrientation = Integer.parseInt(args[4]); + options.setLockedVideoOrientation(lockedVideoOrientation); + // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[4]); + boolean tunnelForward = Boolean.parseBoolean(args[5]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[5]); + Rect crop = parseCrop(args[6]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[6]); + boolean sendFrameMeta = Boolean.parseBoolean(args[7]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[7]); + boolean control = Boolean.parseBoolean(args[8]); options.setControl(control); return options; From d3281f4b67c36164955593ec6b7fcf16e2f95436 Mon Sep 17 00:00:00 2001 From: yangfl Date: Thu, 16 Jan 2020 02:47:34 +0800 Subject: [PATCH 015/214] Show a friendly hint for adb installation Signed-off-by: Romain Vimont --- app/src/command.c | 27 +++++++++++++++++++++++++++ app/src/command.h | 5 +++++ app/src/sys/unix/command.c | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/app/src/command.c b/app/src/command.c index abaa223d..81047b7a 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -55,6 +55,32 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) { return idx; } +static void +show_adb_installation_msg() { +#ifndef __WINDOWS__ + static const struct { + const char *binary; + const char *command; + } pkg_managers[] = { + {"apt", "apt install adb"}, + {"apt-get", "apt-get install adb"}, + {"brew", "brew cask install android-platform-tools"}, + {"dnf", "dnf install android-tools"}, + {"emerge", "emerge dev-util/android-tools"}, + {"pacman", "pacman -S android-tools"}, + }; + for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { + if (cmd_search(pkg_managers[i].binary)) { + LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); + return; + } + } +#endif + + LOGI("You may download and install 'adb' from " + "https://developer.android.com/studio/releases/platform-tools"); +} + static void show_adb_err_msg(enum process_result err, const char *const argv[]) { char buf[512]; @@ -68,6 +94,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { LOGE("Command not found: %s", buf); LOGE("(make 'adb' accessible from your PATH or define its full" "path in the ADB environment variable)"); + show_adb_installation_msg(); break; case PROCESS_SUCCESS: // do nothing diff --git a/app/src/command.h b/app/src/command.h index 9fc81c1c..28f9fbcf 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -43,6 +43,11 @@ enum process_result { PROCESS_ERROR_MISSING_BINARY, }; +#ifndef __WINDOWS__ +bool +cmd_search(const char *file); +#endif + enum process_result cmd_execute(const char *const argv[], process_t *process); diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index af5d4b2f..a60e21bc 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,42 @@ #include "util/log.h" +bool +cmd_search(const char *file) { + char *path = getenv("PATH"); + if (!path) + return false; + path = strdup(path); + if (!path) + return false; + + bool ret = false; + size_t file_len = strlen(file); + char *saveptr; + for (char *dir = strtok_r(path, ":", &saveptr); dir; + dir = strtok_r(NULL, ":", &saveptr)) { + size_t dir_len = strlen(dir); + char *fullpath = malloc(dir_len + file_len + 2); + if (!fullpath) + continue; + memcpy(fullpath, dir, dir_len); + fullpath[dir_len] = '/'; + memcpy(fullpath + dir_len + 1, file, file_len + 1); + + struct stat sb; + bool fullpath_executable = stat(fullpath, &sb) == 0 && + sb.st_mode & S_IXUSR; + free(fullpath); + if (fullpath_executable) { + ret = true; + break; + } + } + + free(path); + return ret; +} + enum process_result cmd_execute(const char *const argv[], pid_t *pid) { int fd[2]; From da18c9cdab1c40c605bb63105e92b40284704867 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Mar 2020 21:56:48 +0100 Subject: [PATCH 016/214] Remove useles import --- server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 3e9772ee..0ef61e9a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -6,7 +6,6 @@ import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; -import android.os.Build; import android.os.IBinder; import android.view.Surface; From 63286424bb5a21cbdc2e94a4bf493abe07ea1e98 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Mar 2020 21:44:20 +0100 Subject: [PATCH 017/214] Compute all screen info from ScreenInfo Screen information was partially initialized from Device. Move this initialization to ScreenInfo. --- .../java/com/genymobile/scrcpy/Device.java | 60 +--------------- .../com/genymobile/scrcpy/ScreenInfo.java | 69 ++++++++++++++++++- 2 files changed, 70 insertions(+), 59 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 7ccd0403..c60761cf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -28,7 +28,8 @@ public final class Device { public Device(Options options) { lockedVideoOrientation = options.getLockedVideoOrientation(); - screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize()); registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { @@ -48,59 +49,6 @@ public final class Device { return screenInfo; } - private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); - int rotation = displayInfo.getRotation(); - Size deviceSize = displayInfo.getSize(); - Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); - if (crop != null) { - if (rotation % 2 != 0) { // 180s preserve dimensions - // the crop (provided by the user) is expressed in the natural orientation - crop = flipRect(crop); - } - if (!contentRect.intersect(crop)) { - // intersect() changes contentRect so that it is intersected with crop - Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); - contentRect = new Rect(); // empty - } - } - - Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); - return new ScreenInfo(contentRect, videoSize, rotation); - } - - private static String formatCrop(Rect rect) { - return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; - } - - @SuppressWarnings("checkstyle:MagicNumber") - private static Size computeVideoSize(int w, int h, int maxSize) { - // Compute the video size and the padding of the content inside this video. - // Principle: - // - scale down the great side of the screen to maxSize (if necessary); - // - scale down the other side so that the aspect ratio is preserved; - // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) - w &= ~7; // in case it's not a multiple of 8 - h &= ~7; - if (maxSize > 0) { - if (BuildConfig.DEBUG && maxSize % 8 != 0) { - throw new AssertionError("Max size must be a multiple of 8"); - } - boolean portrait = h > w; - int major = portrait ? h : w; - int minor = portrait ? w : h; - if (major > maxSize) { - int minorExact = minor * maxSize / major; - // +4 to round the value to the nearest multiple of 8 - minor = (minorExact + 4) & ~7; - major = maxSize; - } - w = portrait ? minor : major; - h = portrait ? major : minor; - } - return new Size(w, h); - } - /** * Return the rotation to apply to the device rotation to get the requested locked video orientation * @@ -227,8 +175,4 @@ public final class Device { wm.thawRotation(); } } - - static Rect flipRect(Rect crop) { - return new Rect(crop.top, crop.left, crop.bottom, crop.right); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index 0f01615e..6dd8460c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -3,8 +3,19 @@ package com.genymobile.scrcpy; import android.graphics.Rect; public final class ScreenInfo { + /** + * Device (physical) size, possibly cropped + */ private final Rect contentRect; // device size, possibly cropped + + /** + * Video size, possibly smaller than the device size, already taking the device rotation and crop into account + */ private final Size videoSize; + + /** + * Device rotation, related to the natural device orientation (0, 1, 2 or 3) + */ private final int rotation; public ScreenInfo(Rect contentRect, Size videoSize, int rotation) { @@ -34,7 +45,7 @@ public final class ScreenInfo { Rect newContentRect; Size newVideoSize; if (orientationChanged) { - newContentRect = Device.flipRect(contentRect); + newContentRect = flipRect(contentRect); newVideoSize = videoSize.rotate(); } else { newContentRect = contentRect; @@ -42,4 +53,60 @@ public final class ScreenInfo { } return new ScreenInfo(newContentRect, newVideoSize, newRotation); } + + public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize) { + int rotation = displayInfo.getRotation(); + Size deviceSize = displayInfo.getSize(); + Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); + if (crop != null) { + if (rotation % 2 != 0) { // 180s preserve dimensions + // the crop (provided by the user) is expressed in the natural orientation + crop = flipRect(crop); + } + if (!contentRect.intersect(crop)) { + // intersect() changes contentRect so that it is intersected with crop + Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); + contentRect = new Rect(); // empty + } + } + + Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + return new ScreenInfo(contentRect, videoSize, rotation); + } + + private static String formatCrop(Rect rect) { + return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static Size computeVideoSize(int w, int h, int maxSize) { + // Compute the video size and the padding of the content inside this video. + // Principle: + // - scale down the great side of the screen to maxSize (if necessary); + // - scale down the other side so that the aspect ratio is preserved; + // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) + w &= ~7; // in case it's not a multiple of 8 + h &= ~7; + if (maxSize > 0) { + if (BuildConfig.DEBUG && maxSize % 8 != 0) { + throw new AssertionError("Max size must be a multiple of 8"); + } + boolean portrait = h > w; + int major = portrait ? h : w; + int minor = portrait ? w : h; + if (major > maxSize) { + int minorExact = minor * maxSize / major; + // +4 to round the value to the nearest multiple of 8 + minor = (minorExact + 4) & ~7; + major = maxSize; + } + w = portrait ? minor : major; + h = portrait ? major : minor; + } + return new Size(w, h); + } + + private static Rect flipRect(Rect crop) { + return new Rect(crop.top, crop.left, crop.bottom, crop.right); + } } From c5f5d1e45669d676f74eaebb532f55aae99c65f4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Mar 2020 22:04:56 +0100 Subject: [PATCH 018/214] Rename "rotation" to "device rotation" This paves the way to reduce confusion in ScreenInfo when it will handle locked video orientation. --- .../java/com/genymobile/scrcpy/Device.java | 4 ++-- .../com/genymobile/scrcpy/ScreenEncoder.java | 2 +- .../java/com/genymobile/scrcpy/ScreenInfo.java | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index c60761cf..1eebc679 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -34,7 +34,7 @@ public final class Device { @Override public void onRotationChanged(int rotation) throws RemoteException { synchronized (Device.this) { - screenInfo = screenInfo.withRotation(rotation); + screenInfo = screenInfo.withDeviceRotation(rotation); // notify if (rotationListener != null) { @@ -83,7 +83,7 @@ public final class Device { ScreenInfo screenInfo = getScreenInfo(); // read with synchronization Size videoSize = screenInfo.getVideoSize(); - int deviceRotation = screenInfo.getRotation(); + int deviceRotation = screenInfo.getDeviceRotation(); int reverseVideoRotation = getReverseVideoRotation(deviceRotation); // reverse the video rotation to apply the events Position devicePosition = position.rotate(reverseVideoRotation); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 0ef61e9a..61bccc93 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -67,7 +67,7 @@ public class ScreenEncoder implements Device.RotationListener { ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); Rect videoRect = screenInfo.getVideoSize().toRect(); - int videoRotation = device.getVideoRotation(screenInfo.getRotation()); + int videoRotation = device.getVideoRotation(screenInfo.getDeviceRotation()); setSize(format, videoRotation, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index 6dd8460c..3e7cadc3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -16,12 +16,12 @@ public final class ScreenInfo { /** * Device rotation, related to the natural device orientation (0, 1, 2 or 3) */ - private final int rotation; + private final int deviceRotation; - public ScreenInfo(Rect contentRect, Size videoSize, int rotation) { + public ScreenInfo(Rect contentRect, Size videoSize, int deviceRotation) { this.contentRect = contentRect; this.videoSize = videoSize; - this.rotation = rotation; + this.deviceRotation = deviceRotation; } public Rect getContentRect() { @@ -32,16 +32,16 @@ public final class ScreenInfo { return videoSize; } - public int getRotation() { - return rotation; + public int getDeviceRotation() { + return deviceRotation; } - public ScreenInfo withRotation(int newRotation) { - if (newRotation == rotation) { + public ScreenInfo withDeviceRotation(int newDeviceRotation) { + if (newDeviceRotation == deviceRotation) { return this; } // true if changed between portrait and landscape - boolean orientationChanged = (rotation + newRotation) % 2 != 0; + boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; Rect newContentRect; Size newVideoSize; if (orientationChanged) { @@ -51,7 +51,7 @@ public final class ScreenInfo { newContentRect = contentRect; newVideoSize = videoSize; } - return new ScreenInfo(newContentRect, newVideoSize, newRotation); + return new ScreenInfo(newContentRect, newVideoSize, newDeviceRotation); } public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize) { From ae2d094362fbdc4d402b0315d981015ffe5c4967 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Mar 2020 22:01:35 +0100 Subject: [PATCH 019/214] Handle locked video orientation from ScreenInfo Centralize video size management in ScreenInfo. This allows to always send the correct initial video size to the client if the video orientation is locked. --- .../java/com/genymobile/scrcpy/Device.java | 45 ++--------- .../com/genymobile/scrcpy/ScreenEncoder.java | 20 +++-- .../com/genymobile/scrcpy/ScreenInfo.java | 74 ++++++++++++++++--- 3 files changed, 80 insertions(+), 59 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 1eebc679..1b1fbf7d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -22,14 +22,12 @@ public final class Device { private final ServiceManager serviceManager = new ServiceManager(); - private final int lockedVideoOrientation; private ScreenInfo screenInfo; private RotationListener rotationListener; public Device(Options options) { - lockedVideoOrientation = options.getLockedVideoOrientation(); DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize()); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { @@ -49,55 +47,28 @@ public final class Device { return screenInfo; } - /** - * Return the rotation to apply to the device rotation to get the requested locked video orientation - * - * @param deviceRotation the device rotation - * @return the rotation offset - */ - public int getVideoRotation(int deviceRotation) { - if (lockedVideoOrientation == -1) { - // no offset - return 0; - } - return (deviceRotation + 4 - lockedVideoOrientation) % 4; - } - - /** - * Return the rotation to apply to the requested locked video orientation to get the device rotation - * - * @param deviceRotation the device rotation - * @return the (reverse) rotation offset - */ - private int getReverseVideoRotation(int deviceRotation) { - if (lockedVideoOrientation == -1) { - // no offset - return 0; - } - return (lockedVideoOrientation + 4 - deviceRotation) % 4; - } - public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") ScreenInfo screenInfo = getScreenInfo(); // read with synchronization - Size videoSize = screenInfo.getVideoSize(); - int deviceRotation = screenInfo.getDeviceRotation(); - int reverseVideoRotation = getReverseVideoRotation(deviceRotation); + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); + + int reverseVideoRotation = screenInfo.getReverseVideoRotation(); // reverse the video rotation to apply the events Position devicePosition = position.rotate(reverseVideoRotation); Size clientVideoSize = devicePosition.getScreenSize(); - if (!videoSize.equals(clientVideoSize)) { + if (!unlockedVideoSize.equals(clientVideoSize)) { // The client sends a click relative to a video with wrong dimensions, // the device may have been rotated since the event was generated, so ignore the event return null; } Rect contentRect = screenInfo.getContentRect(); Point point = devicePosition.getPoint(); - int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); - int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); return new Point(convertedX, convertedY); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 61bccc93..e99084af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -66,12 +66,15 @@ public class ScreenEncoder implements Device.RotationListener { IBinder display = createDisplay(); ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); + // include the locked video orientation Rect videoRect = screenInfo.getVideoSize().toRect(); - int videoRotation = device.getVideoRotation(screenInfo.getDeviceRotation()); - setSize(format, videoRotation, videoRect.width(), videoRect.height()); + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, videoRotation, contentRect, videoRect); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect); codec.start(); try { alive = encode(codec, fd); @@ -170,14 +173,9 @@ public class ScreenEncoder implements Device.RotationListener { codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); } - private static void setSize(MediaFormat format, int orientation, int width, int height) { - if (orientation % 2 == 0) { - format.setInteger(MediaFormat.KEY_WIDTH, width); - format.setInteger(MediaFormat.KEY_HEIGHT, height); - return; - } - format.setInteger(MediaFormat.KEY_WIDTH, height); - format.setInteger(MediaFormat.KEY_HEIGHT, width); + private static void setSize(MediaFormat format, int width, int height) { + format.setInteger(MediaFormat.KEY_WIDTH, width); + format.setInteger(MediaFormat.KEY_HEIGHT, height); } private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect) { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index 3e7cadc3..0204de82 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -9,27 +9,53 @@ public final class ScreenInfo { private final Rect contentRect; // device size, possibly cropped /** - * Video size, possibly smaller than the device size, already taking the device rotation and crop into account + * Video size, possibly smaller than the device size, already taking the device rotation and crop into account. + *

+ * However, it does not include the locked video orientation. */ - private final Size videoSize; + private final Size unlockedVideoSize; /** * Device rotation, related to the natural device orientation (0, 1, 2 or 3) */ private final int deviceRotation; - public ScreenInfo(Rect contentRect, Size videoSize, int deviceRotation) { + /** + * The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) + */ + private final int lockedVideoOrientation; + + public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) { this.contentRect = contentRect; - this.videoSize = videoSize; + this.unlockedVideoSize = unlockedVideoSize; this.deviceRotation = deviceRotation; + this.lockedVideoOrientation = lockedVideoOrientation; } public Rect getContentRect() { return contentRect; } + /** + * Return the video size as if locked video orientation was not set. + * + * @return the unlocked video size + */ + public Size getUnlockedVideoSize() { + return unlockedVideoSize; + } + + /** + * Return the actual video size if locked video orientation is set. + * + * @return the actual video size + */ public Size getVideoSize() { - return videoSize; + if (getVideoRotation() % 2 == 0) { + return unlockedVideoSize; + } + + return unlockedVideoSize.rotate(); } public int getDeviceRotation() { @@ -43,18 +69,18 @@ public final class ScreenInfo { // true if changed between portrait and landscape boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; Rect newContentRect; - Size newVideoSize; + Size newUnlockedVideoSize; if (orientationChanged) { newContentRect = flipRect(contentRect); - newVideoSize = videoSize.rotate(); + newUnlockedVideoSize = unlockedVideoSize.rotate(); } else { newContentRect = contentRect; - newVideoSize = videoSize; + newUnlockedVideoSize = unlockedVideoSize; } - return new ScreenInfo(newContentRect, newVideoSize, newDeviceRotation); + return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); } - public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize) { + public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { int rotation = displayInfo.getRotation(); Size deviceSize = displayInfo.getSize(); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); @@ -71,7 +97,7 @@ public final class ScreenInfo { } Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); - return new ScreenInfo(contentRect, videoSize, rotation); + return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); } private static String formatCrop(Rect rect) { @@ -109,4 +135,30 @@ public final class ScreenInfo { private static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } + + /** + * Return the rotation to apply to the device rotation to get the requested locked video orientation + * + * @return the rotation offset + */ + public int getVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (deviceRotation + 4 - lockedVideoOrientation) % 4; + } + + /** + * Return the rotation to apply to the requested locked video orientation to get the device rotation + * + * @return the (reverse) rotation offset + */ + public int getReverseVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (lockedVideoOrientation + 4 - deviceRotation) % 4; + } } From cd69eb4a4fecf8167208399def4ef536b59c9d22 Mon Sep 17 00:00:00 2001 From: Jaafar Biyadi Date: Sat, 29 Feb 2020 18:33:17 +0100 Subject: [PATCH 020/214] Handle NumPad events when NumLock is disabled PR #1188 Fixes #1048 Signed-off-by: Romain Vimont --- app/src/event_converter.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 80ead615..1054dcf9 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -94,6 +94,23 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_UP, AKEYCODE_DPAD_UP); } + if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { + // Handle Numpad events when Num Lock is disabled + // If SHIFT is pressed, a text event will be sent instead + switch(from) { + MAP(SDLK_KP_0, AKEYCODE_INSERT); + MAP(SDLK_KP_1, AKEYCODE_MOVE_END); + MAP(SDLK_KP_2, AKEYCODE_DPAD_DOWN); + MAP(SDLK_KP_3, AKEYCODE_PAGE_DOWN); + MAP(SDLK_KP_4, AKEYCODE_DPAD_LEFT); + MAP(SDLK_KP_6, AKEYCODE_DPAD_RIGHT); + MAP(SDLK_KP_7, AKEYCODE_MOVE_HOME); + MAP(SDLK_KP_8, AKEYCODE_DPAD_UP); + MAP(SDLK_KP_9, AKEYCODE_PAGE_UP); + MAP(SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL); + } + } + if (prefer_text) { // do not forward alpha and space key events return false; From 902b99174df8ffc1fe7548399c19e446aa5488b6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 19 Mar 2020 19:15:43 +0100 Subject: [PATCH 021/214] Fix server debugger for Android >= 9 Add a compilation flag to select the debugger method to use: - old: Android < 9 - new: Android >= 9 See --- DEVELOP.md | 9 +++++++++ app/meson.build | 3 +++ app/src/server.c | 6 ++++++ meson_options.txt | 1 + 4 files changed, 19 insertions(+) diff --git a/DEVELOP.md b/DEVELOP.md index 0258782f..4d8acc59 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -282,6 +282,15 @@ meson x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` +If your device runs Android 8 or below, set the `server_debugger_method` to +`old` in addition: + +```bash +meson x -Dserver_debugger=true -Dserver_debugger_method=old +# or, if x is already configured +meson configure x -Dserver_debugger=true -Dserver_debugger_method=old +``` + Then recompile. When you start scrcpy, it will start a debugger on port 5005 on the device. diff --git a/app/meson.build b/app/meson.build index 3df2f35c..49c4683f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -124,6 +124,9 @@ conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) +# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) +conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') diff --git a/app/src/server.c b/app/src/server.c index 4b2c1866..a8d598e4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -244,7 +244,13 @@ execute_server(struct server *server, const struct server_params *params) { "app_process", #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" +# ifdef SERVER_DEBUGGER_METHOD_NEW + /* Android 9 and above */ + "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address=" +# else + /* Android 8 and below */ "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" +# endif SERVER_DEBUGGER_PORT, #endif "/", // unused diff --git a/meson_options.txt b/meson_options.txt index 4cf4a8bf..c213e7dd 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,3 +6,4 @@ option('prebuilt_server', type: 'string', description: 'Path of the prebuilt ser option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') +option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') From 600df37753856122f004b79edff980a375ec6447 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 19 Mar 2020 19:16:08 +0100 Subject: [PATCH 022/214] Mention AutoAdb in README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 998caae8..b190406c 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,16 @@ scrcpy -s 192.168.0.1:5555 # short version You can start several instances of _scrcpy_ for several devices. +#### Autostart on device connection + +You could use [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/usbaudio + #### SSH tunnel To connect to a remote device, it is possible to connect a local `adb` client to From 7bb91638ad80ba20700529cf336ea533d8f058e7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Feb 2020 18:51:36 +0100 Subject: [PATCH 023/214] Improve FAQ --- FAQ.md | 149 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 22 deletions(-) diff --git a/FAQ.md b/FAQ.md index 49382471..5135cc47 100644 --- a/FAQ.md +++ b/FAQ.md @@ -3,19 +3,102 @@ Here are the common reported problems and their status. -### On Windows, my device is not detected +## `adb` issues -The most common is your device not being detected by `adb`, or is unauthorized. -Check everything is ok by calling: +`scrcpy` execute `adb` commands to initialize the connection with the device. If +`adb` fails, then scrcpy will not work. - adb devices +In that case, it will print this error: -Windows may need some [drivers] to detect your device. +> ERROR: "adb push" returned with value 1 + +This is typically not a bug in _scrcpy_, but a problem in your environment. + +To find out the cause, execute: + +```bash +adb devices +``` + +### `adb` not found + +You need `adb` accessible from your `PATH`. + +On Windows, the current directory is in your `PATH`, and `adb.exe` is included +in the release, so it should work out-of-the-box. + + +### Device unauthorized + +Check [stackoverflow][device-unauthorized]. + +[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized + + +### Device not detected + +If your device is not detected, you may need some [drivers] (on Windows). [drivers]: https://developer.android.com/studio/run/oem-usb.html -### I can only mirror, I cannot interact with the device +### Several devices connected + +If several devices are connected, you will encounter this error: + +> adb: error: failed to get feature set: more than one device/emulator + +the identifier of the device you want to mirror must be provided: + +```bash +scrcpy -s 01234567890abcdef +``` + +Note that if your device is connected over TCP/IP, you'll get this message: + +> adb: error: more than one device/emulator +> ERROR: "adb reverse" returned with value 1 +> WARN: 'adb reverse' failed, fallback to 'adb forward' + +This is expected (due to a bug on old Android versions, see [#5]), but in that +case, scrcpy fallbacks to a different method, which should work. + +[#5]: https://github.com/Genymobile/scrcpy/issues/5 + + +### Conflicts between adb versions + +> adb server version (41) doesn't match this client (39); killing... + +This error occurs when you use several `adb` versions simultaneously. You must +find the program using a different `adb` version, and use the same `adb` version +everywhere. + +You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to +use a specific `adb` binary, by setting the `ADB` environment variable: + +```bash +set ADB=/path/to/your/adb +scrcpy +``` + + +### Device disconnected + +If _scrcpy_ stops itself with the warning "Device disconnected", then the +`adb` connection has been closed. + +Try with another USB cable or plug it into another USB port. See [#281] and +[#283]. + +[#281]: https://github.com/Genymobile/scrcpy/issues/281 +[#283]: https://github.com/Genymobile/scrcpy/issues/283 + + + +## Control issues + +### Mouse and keyboard do not work On some devices, you may need to enable an option to allow [simulating input]. In developer options, enable: @@ -29,18 +112,25 @@ In developer options, enable: ### Mouse clicks at wrong location On MacOS, with HiDPI support and multiple screens, input location are wrongly -scaled. See [issue 15]. +scaled. See [#15]. -[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 +[#15]: https://github.com/Genymobile/scrcpy/issues/15 -A workaround is to build with HiDPI support disabled: +Open _scrcpy_ directly on the monitor you use it. -```bash -meson x --buildtype release -Dhidpi_support=false -``` -However, the video will be displayed at lower resolution. +### Special characters do not work +Injecting text input is [limited to ASCII characters][text-input]. A trick +allows to also inject some [accented characters][accented-characters], but +that's all. See [#37]. + +[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode +[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters +[#37]: https://github.com/Genymobile/scrcpy/issues/37 + + +## Client issues ### The quality is low on HiDPI display @@ -51,6 +141,11 @@ On Windows, you may need to configure the [scaling behavior]. [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 +If your computer definition is far smaller than your screen, then you'll get +poor quality. See [#40]. + +[#40]: https://github.com/Genymobile/scrcpy/issues/40 + ### KWin compositor crashes @@ -61,19 +156,29 @@ As a workaround, [disable "Block compositing"][kwin]. [kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 -### I get an error "Could not open video stream" +## Crashes + +### Exception There may be many reasons. One common cause is that the hardware encoder of your device is not able to encode at the given definition: -``` -ERROR: Exception on thread Thread[main,5,main] -android.media.MediaCodec$CodecException: Error 0xfffffc0e -... -Exit due to uncaughtException in main thread: -ERROR: Could not open video stream -INFO: Initial texture: 1080x2336 -``` +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> android.media.MediaCodec$CodecException: Error 0xfffffc0e +> ... +> Exit due to uncaughtException in main thread: +> ERROR: Could not open video stream +> INFO: Initial texture: 1080x2336 +> ``` + +or + +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> java.lang.IllegalStateException +> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) +> ``` Just try with a lower definition: From a0af402d96849f1573b1361523ac96772952e480 Mon Sep 17 00:00:00 2001 From: Tzah Mazuz Date: Mon, 16 Mar 2020 11:23:11 +0200 Subject: [PATCH 024/214] Fix the printed versions (were opposite) PR #1224 Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/Server.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 2b0d32a2..ef4214c4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -77,7 +77,7 @@ public final class Server { String clientVersion = args[0]; if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { throw new IllegalArgumentException( - "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); + "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } if (args.length != 9) { From 566ba766af6f84e04d0c64533c88bbbc5c4ec35a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Mar 2020 22:43:53 +0100 Subject: [PATCH 025/214] Remove unused constant It has not been removed when mouse and touch events have been merged. --- .../main/java/com/genymobile/scrcpy/ControlMessageReader.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 726b5659..70459913 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -9,7 +9,6 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; - private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; From 89d1602185066ff540b6525bf99eb894004aad04 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Mar 2020 22:45:43 +0100 Subject: [PATCH 026/214] Fix expected message length for touch events The expected length for a touch event control message was incorrect. As a consequence, a BufferUnderflowException could occur. Fixes #1245 --- .../main/java/com/genymobile/scrcpy/ControlMessageReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 70459913..065688a6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; - private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; + private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; From 3504c0016b835dab4e74e1a05bfadedcd325c873 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Mar 2020 22:48:01 +0100 Subject: [PATCH 027/214] Add tests for control message length This will avoid regressions for #1245. --- .../com/genymobile/scrcpy/ControlMessageReader.java | 8 ++++---- .../genymobile/scrcpy/ControlMessageReaderTest.java | 12 ++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 065688a6..e4347ddb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -8,10 +8,10 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { - private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; - private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; - private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; - private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; + static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; public static final int TEXT_MAX_LENGTH = 300; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 5e663bb9..2f95dfe1 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -28,6 +28,9 @@ public class ControlMessageReaderTest { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -95,6 +98,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -126,6 +132,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -233,6 +242,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); From dc7c677728cdeb7471745a2aff9f63ebe8d14f53 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Mar 2020 17:54:06 +0100 Subject: [PATCH 028/214] Accept negative window position It seems to work on some window managers. Fixes #1242 --- app/scrcpy.1 | 4 ++-- app/src/cli.c | 15 ++++++++++++--- app/src/scrcpy.h | 8 ++++---- app/src/screen.c | 6 ++++-- app/src/screen.h | 3 +++ 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b3c57064..55b7a631 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -131,13 +131,13 @@ Set a custom window title. .BI "\-\-window\-x " value Set the initial window horizontal position. -Default is -1 (automatic).\n +Default is "auto".\n .TP .BI "\-\-window\-y " value Set the initial window vertical position. -Default is -1 (automatic).\n +Default is "auto".\n .TP .BI "\-\-window\-width " value diff --git a/app/src/cli.c b/app/src/cli.c index 4b093c49..48876a29 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1,5 +1,6 @@ #include "cli.h" +#include #include #include #include @@ -116,11 +117,11 @@ scrcpy_print_usage(const char *arg0) { "\n" " --window-x value\n" " Set the initial window horizontal position.\n" - " Default is -1 (automatic).\n" + " Default is \"auto\".\n" "\n" " --window-y value\n" " Set the initial window vertical position.\n" - " Default is -1 (automatic).\n" + " Default is \"auto\".\n" "\n" " --window-width value\n" " Set the initial window width.\n" @@ -302,8 +303,16 @@ parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) { static bool parse_window_position(const char *s, int16_t *position) { + // special value for "auto" + static_assert(WINDOW_POSITION_UNDEFINED == -0x8000); + + if (!strcmp(s, "auto")) { + *position = WINDOW_POSITION_UNDEFINED; + return true; + } + long value; - bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, + bool ok = parse_integer_arg(s, &value, false, -0x7FFF, 0x7FFF, "window position"); if (!ok) { return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index e29298f2..014f2cfe 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -21,8 +21,8 @@ struct scrcpy_options { uint32_t bit_rate; uint16_t max_fps; int8_t lock_video_orientation; - int16_t window_x; - int16_t window_y; + int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto" + int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; uint16_t window_height; bool show_touches; @@ -51,8 +51,8 @@ struct scrcpy_options { .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ - .window_x = -1, \ - .window_y = -1, \ + .window_x = WINDOW_POSITION_UNDEFINED, \ + .window_y = WINDOW_POSITION_UNDEFINED, \ .window_width = 0, \ .window_height = 0, \ .show_touches = false, \ diff --git a/app/src/screen.c b/app/src/screen.c index beb10754..03e2c3a1 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -186,8 +186,10 @@ screen_init_rendering(struct screen *screen, const char *window_title, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = window_y != -1 ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; + int x = window_x != WINDOW_POSITION_UNDEFINED + ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; + int y = window_y != WINDOW_POSITION_UNDEFINED + ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, window_flags); diff --git a/app/src/screen.h b/app/src/screen.h index 2346ff15..c31f32c5 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -8,6 +8,8 @@ #include "config.h" #include "common.h" +#define WINDOW_POSITION_UNDEFINED (-0x8000) + struct video_buffer; struct screen { @@ -53,6 +55,7 @@ void screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) +// window_x and window_y accept WINDOW_POSITION_UNDEFINED bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, From e050cfdcd6ab5c0133d8b98baaa38c45a5b604aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Mar 2020 14:01:56 +0100 Subject: [PATCH 029/214] Fix static_assert() parameters In C11, static_assert() expects a message. --- app/src/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 48876a29..d2cd93b7 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -304,7 +304,7 @@ parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) { static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" - static_assert(WINDOW_POSITION_UNDEFINED == -0x8000); + static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); if (!strcmp(s, "auto")) { *position = WINDOW_POSITION_UNDEFINED; From 4adf5fde6dfb91d7af21103a2620f28707bc8334 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Mar 2020 15:52:02 +0100 Subject: [PATCH 030/214] Log device details on server start --- server/src/main/java/com/genymobile/scrcpy/Server.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ef4214c4..db093053 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -16,6 +16,7 @@ public final class Server { } private static void scrcpy(Options options) throws IOException { + Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { From 5031b2c8ff4285a43ca445f53adaaa7fe635d06f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Mar 2020 22:08:16 +0100 Subject: [PATCH 031/214] Remove MagicNumber checkstyle There are a lot of "magic numbers" that we really don't want to extract as a constant. Until now, many @SuppressWarnings annotations were added, but it makes no sense to check for magic number if we silent the warnings everywhere. --- config/checkstyle/checkstyle.xml | 5 ----- .../java/com/genymobile/scrcpy/ControlMessageReader.java | 3 --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 1 - .../main/java/com/genymobile/scrcpy/DesktopConnection.java | 1 - .../main/java/com/genymobile/scrcpy/DeviceMessageWriter.java | 1 - .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 1 - server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java | 1 - server/src/main/java/com/genymobile/scrcpy/Server.java | 3 --- server/src/main/java/com/genymobile/scrcpy/StringUtils.java | 1 - .../java/com/genymobile/scrcpy/ControlMessageReaderTest.java | 2 -- .../src/test/java/com/genymobile/scrcpy/StringUtilsTest.java | 1 - 11 files changed, 20 deletions(-) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 798814d9..812d060b 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -129,11 +129,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index e4347ddb..29172c08 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -121,7 +121,6 @@ public class ControlMessageReader { return ControlMessage.createInjectText(text); } - @SuppressWarnings("checkstyle:MagicNumber") private ControlMessage parseInjectTouchEvent() { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; @@ -171,12 +170,10 @@ public class ControlMessageReader { return new Position(x, y, screenWidth, screenHeight); } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(short value) { return value & 0xffff; } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(byte value) { return value & 0xff; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index dc0fa67b..7595a9d7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -47,7 +47,6 @@ public class Controller { } } - @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { // on start, power on the device if (!device.isScreenOn()) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index a725d83d..0ec43040 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -84,7 +84,6 @@ public final class DesktopConnection implements Closeable { controlSocket.close(); } - @SuppressWarnings("checkstyle:MagicNumber") private void send(String deviceName, int width, int height) throws IOException { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index e2a3a1a2..6c7f3634 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -13,7 +13,6 @@ public class DeviceMessageWriter { private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - @SuppressWarnings("checkstyle:MagicNumber") public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index e99084af..d75833f3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -145,7 +145,6 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType("video/avc"); } - @SuppressWarnings("checkstyle:MagicNumber") private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, "video/avc"); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index 0204de82..10acfb50 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -104,7 +104,6 @@ public final class ScreenInfo { return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; } - @SuppressWarnings("checkstyle:MagicNumber") private static Size computeVideoSize(int w, int h, int maxSize) { // Compute the video size and the padding of the content inside this video. // Principle: diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index db093053..c4954429 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -69,7 +69,6 @@ public final class Server { }).start(); } - @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { if (args.length < 1) { throw new IllegalArgumentException("Missing client version"); @@ -115,7 +114,6 @@ public final class Server { return options; } - @SuppressWarnings("checkstyle:MagicNumber") private static Rect parseCrop(String crop) { if ("-".equals(crop)) { return null; @@ -140,7 +138,6 @@ public final class Server { } } - @SuppressWarnings("checkstyle:MagicNumber") private static void suggestFix(Throwable e) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (e instanceof MediaCodec.CodecException) { diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java index 199fc8c1..dac05466 100644 --- a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -5,7 +5,6 @@ public final class StringUtils { // not instantiable } - @SuppressWarnings("checkstyle:MagicNumber") public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { int len = utf8.length; if (len <= maxLength) { diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 2f95dfe1..1905942b 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -80,7 +80,6 @@ public class ControlMessageReaderTest { } @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testParseTouchEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); @@ -116,7 +115,6 @@ public class ControlMessageReaderTest { } @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testParseScrollEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java index 7d89ee64..89799c5e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -8,7 +8,6 @@ import java.nio.charset.StandardCharsets; public class StringUtilsTest { @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testUtf8Truncate() { String s = "aÉbÔc"; byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); From 4150eedcdf1652fd131480383850882f95a0ea7d Mon Sep 17 00:00:00 2001 From: e_vigurskiy Date: Mon, 24 Feb 2020 14:16:38 +0300 Subject: [PATCH 032/214] Add display id parameter Add --display command line parameter to specify a display id. PR #1238 Signed-off-by: Romain Vimont --- README.md | 15 +++++ app/scrcpy.1 | 9 +++ app/src/cli.c | 28 ++++++++++ app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 + app/src/server.c | 3 + app/src/server.h | 1 + .../com/genymobile/scrcpy/Controller.java | 24 ++++++-- .../java/com/genymobile/scrcpy/Device.java | 56 ++++++++++++++++++- .../com/genymobile/scrcpy/DisplayInfo.java | 23 +++++++- .../scrcpy/InvalidDisplayIdException.java | 21 +++++++ .../java/com/genymobile/scrcpy/Options.java | 9 +++ .../com/genymobile/scrcpy/ScreenEncoder.java | 8 ++- .../java/com/genymobile/scrcpy/Server.java | 17 +++++- .../scrcpy/wrappers/DisplayManager.java | 19 ++++++- .../scrcpy/wrappers/InputManager.java | 21 +++++++ 16 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java diff --git a/README.md b/README.md index b190406c..96b2f087 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,21 @@ scrcpy --no-control scrcpy -n ``` +#### Display + +If several displays are available, it is possible to select the display to +mirror: + +```bash +scrcpy --display 1 +``` + +The list of display ids can be retrieved by: + +``` +adb shell dumpsys display # search "mDisplayId=" in the output +``` + #### Turn screen off It is possible to turn the device screen off while mirroring on start with a diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 55b7a631..da6c7f2d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -33,6 +33,15 @@ The values are expressed in the device natural orientation (typically, portrait .B \-\-max\-size value is computed on the cropped size. +.TP +.BI "\-\-display " id +Specify the display id to mirror. + +The list of possible display ids can be listed by "adb shell dumpsys display" +(search "mDisplayId=" in the output). + +Default is 0. + .TP .B \-f, \-\-fullscreen Start in fullscreen. diff --git a/app/src/cli.c b/app/src/cli.c index d2cd93b7..bb379f47 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -36,6 +36,15 @@ scrcpy_print_usage(const char *arg0) { " (typically, portrait for a phone, landscape for a tablet).\n" " Any --max-size value is computed on the cropped size.\n" "\n" + " --display id\n" + " Specify the display id to mirror.\n" + "\n" + " The list of possible display ids can be listed by:\n" + " adb shell dumpsys display\n" + " (search \"mDisplayId=\" in the output)\n" + "\n" + " Default is 0.\n" + "\n" " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" @@ -363,6 +372,18 @@ parse_port_range(const char *s, struct port_range *port_range) { return true; } +static bool +parse_display_id(const char *s, uint16_t *display_id) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id"); + if (!ok) { + return false; + } + + *display_id = (uint16_t) value; + return true; +} + static bool parse_record_format(const char *optarg, enum recorder_format *format) { if (!strcmp(optarg, "mp4")) { @@ -407,6 +428,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_BORDERLESS 1011 #define OPT_MAX_FPS 1012 #define OPT_LOCK_VIDEO_ORIENTATION 1013 +#define OPT_DISPLAY_ID 1014 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -414,6 +436,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"bit-rate", required_argument, NULL, 'b'}, {"crop", required_argument, NULL, OPT_CROP}, + {"display", required_argument, NULL, OPT_DISPLAY_ID}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"lock-video-orientation", required_argument, NULL, @@ -462,6 +485,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_CROP: opts->crop = optarg; break; + case OPT_DISPLAY_ID: + if (!parse_display_id(optarg, &opts->display_id)) { + return false; + } + break; case 'f': opts->fullscreen = true; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4d9ad88b..84c87ad8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -286,6 +286,7 @@ scrcpy(const struct scrcpy_options *options) { .max_fps = options->max_fps, .lock_video_orientation = options->lock_video_orientation, .control = options->control, + .display_id = options->display_id, }; if (!server_start(&server, options->serial, ¶ms)) { return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 014f2cfe..b6c52fd7 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -25,6 +25,7 @@ struct scrcpy_options { int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; uint16_t window_height; + uint16_t display_id; bool show_touches; bool fullscreen; bool always_on_top; @@ -55,6 +56,7 @@ struct scrcpy_options { .window_y = WINDOW_POSITION_UNDEFINED, \ .window_width = 0, \ .window_height = 0, \ + .display_id = 0, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ diff --git a/app/src/server.c b/app/src/server.c index a8d598e4..89bf9c72 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -234,10 +234,12 @@ execute_server(struct server *server, const struct server_params *params) { char bit_rate_string[11]; char max_fps_string[6]; char lock_video_orientation_string[3]; + char display_id_string[6]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(max_fps_string, "%"PRIu16, params->max_fps); sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation); + sprintf(display_id_string, "%"PRIu16, params->display_id); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, @@ -264,6 +266,7 @@ execute_server(struct server *server, const struct server_params *params) { params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", + display_id_string, }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " diff --git a/app/src/server.h b/app/src/server.h index d84a5cc8..cc712b63 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,6 +44,7 @@ struct server_params { uint16_t max_fps; int8_t lock_video_orientation; bool control; + uint16_t display_id; }; // init default values diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 7595a9d7..32a18e16 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -75,19 +75,29 @@ public class Controller { ControlMessage msg = connection.receiveControlMessage(); switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + if (device.supportsInputEvents()) { + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + } break; case ControlMessage.TYPE_INJECT_TEXT: - injectText(msg.getText()); + if (device.supportsInputEvents()) { + injectText(msg.getText()); + } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + if (device.supportsInputEvents()) { + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + if (device.supportsInputEvents()) { + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - pressBackOrTurnScreenOn(); + if (device.supportsInputEvents()) { + pressBackOrTurnScreenOn(); + } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: device.expandNotificationPanel(); @@ -103,7 +113,9 @@ public class Controller { device.setClipboardText(msg.getText()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - device.setScreenPowerMode(msg.getAction()); + if (device.supportsInputEvents()) { + device.setScreenPowerMode(msg.getAction()); + } break; case ControlMessage.TYPE_ROTATE_DEVICE: device.rotateDevice(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 1b1fbf7d..64d77de3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; @@ -25,9 +26,35 @@ public final class Device { private ScreenInfo screenInfo; private RotationListener rotationListener; + /** + * Logical display identifier + */ + private final int displayId; + + /** + * The surface flinger layer stack associated with this logical display + */ + private final int layerStack; + + /** + * The FLAG_PRESENTATION from the DisplayInfo + */ + private final boolean isPresentationDisplay; + public Device(Options options) { - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); + displayId = options.getDisplayId(); + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + int[] displayIds = serviceManager.getDisplayManager().getDisplayIds(); + throw new InvalidDisplayIdException(displayId, displayIds); + } + + int displayInfoFlags = displayInfo.getFlags(); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + layerStack = displayInfo.getLayerStack(); + isPresentationDisplay = (displayInfoFlags & DisplayInfo.FLAG_PRESENTATION) != 0; + registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { @@ -41,12 +68,24 @@ public final class Device { } } }); + + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } + + if (!supportsInputEvents()) { + Ln.w("Input events are not supported for displays with FLAG_PRESENTATION enabled for devices with API lower than 29"); + } } public synchronized ScreenInfo getScreenInfo() { return screenInfo; } + public int getLayerStack() { + return layerStack; + } + public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") @@ -76,7 +115,22 @@ public final class Device { return Build.MODEL; } + public boolean supportsInputEvents() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return true; + } + return !isPresentationDisplay; + } + public boolean injectInputEvent(InputEvent inputEvent, int mode) { + if (!supportsInputEvents()) { + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + } + + if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { + return false; + } + return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); } diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java index 639869b5..50bc94aa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java @@ -1,12 +1,25 @@ package com.genymobile.scrcpy; public final class DisplayInfo { + private final int displayId; private final Size size; private final int rotation; + private final int layerStack; + private final int flags; - public DisplayInfo(Size size, int rotation) { + public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; + public static final int FLAG_PRESENTATION = 0x00000008; + + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { + this.displayId = displayId; this.size = size; this.rotation = rotation; + this.layerStack = layerStack; + this.flags = flags; + } + + public int getDisplayId() { + return displayId; } public Size getSize() { @@ -16,5 +29,13 @@ public final class DisplayInfo { public int getRotation() { return rotation; } + + public int getLayerStack() { + return layerStack; + } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java new file mode 100644 index 00000000..81e3b903 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy; + +public class InvalidDisplayIdException extends RuntimeException { + + private final int displayId; + private final int[] availableDisplayIds; + + public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) { + super("There is no display having id " + displayId); + this.displayId = displayId; + this.availableDisplayIds = availableDisplayIds; + } + + public int getDisplayId() { + return displayId; + } + + public int[] getAvailableDisplayIds() { + return availableDisplayIds; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d9a29452..2a4bba33 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -11,6 +11,7 @@ public class Options { private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; + private int displayId; public int getMaxSize() { return maxSize; @@ -75,4 +76,12 @@ public class Options { public void setControl(boolean control) { this.control = control; } + + public int getDisplayId() { + return displayId; + } + + public void setDisplayId(int displayId) { + this.displayId = displayId; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index d75833f3..40457af8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -71,10 +71,12 @@ public class ScreenEncoder implements Device.RotationListener { // does not include the locked video orientation Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); + setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); try { alive = encode(codec, fd); @@ -177,12 +179,12 @@ public class ScreenEncoder implements Device.RotationListener { format.setInteger(MediaFormat.KEY_HEIGHT, height); } - private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect) { + private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); - SurfaceControl.setDisplayLayerStack(display, 0); + SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index c4954429..bd387285 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -80,8 +80,8 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - if (args.length != 9) { - throw new IllegalArgumentException("Expecting 9 parameters"); + if (args.length != 10) { + throw new IllegalArgumentException("Expecting 10 parameters"); } Options options = new Options(); @@ -111,6 +111,9 @@ public final class Server { boolean control = Boolean.parseBoolean(args[8]); options.setControl(control); + int displayId = Integer.parseInt(args[9]); + options.setDisplayId(displayId); + return options; } @@ -149,6 +152,16 @@ public final class Server { } } } + if (e instanceof InvalidDisplayIdException) { + InvalidDisplayIdException idie = (InvalidDisplayIdException) e; + int[] displayIds = idie.getAvailableDisplayIds(); + if (displayIds != null && displayIds.length > 0) { + Ln.e("Try to use one of the available display ids:"); + for (int id : displayIds) { + Ln.e(" scrcpy --display " + id); + } + } + } } public static void main(String... args) throws Exception { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 568afacd..cedb3f47 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -12,15 +12,28 @@ public final class DisplayManager { this.manager = manager; } - public DisplayInfo getDisplayInfo() { + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + if (displayInfo == null) { + return null; + } Class cls = displayInfo.getClass(); // width and height already take the rotation into account int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); - return new DisplayInfo(new Size(width, height), rotation); + int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); + int flags = cls.getDeclaredField("flags").getInt(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public int[] getDisplayIds() { + try { + return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); } catch (Exception e) { throw new AssertionError(e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 44fa613b..36d07353 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -17,6 +17,8 @@ public final class InputManager { private final IInterface manager; private Method injectInputEventMethod; + private static Method setDisplayIdMethod; + public InputManager(IInterface manager) { this.manager = manager; } @@ -37,4 +39,23 @@ public final class InputManager { return false; } } + + private static Method getSetDisplayIdMethod() throws NoSuchMethodException { + if (setDisplayIdMethod == null) { + setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class); + } + return setDisplayIdMethod; + } + + public static boolean setDisplayId(InputEvent inputEvent, int displayId) { + try { + Method method = getSetDisplayIdMethod(); + method.invoke(inputEvent, displayId); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + // just a warning, it might happen on old devices + Ln.w("Cannot associate a display id to the input event"); + return false; + } + } } From 64d5edce92dc27cfef11594a27876cd378225e53 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Mar 2020 23:25:38 +0100 Subject: [PATCH 033/214] Refactor server_start() error handling This avoids cleanup duplication. --- app/src/server.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 89bf9c72..2923c6c7 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -345,30 +345,31 @@ server_start(struct server *server, const char *serial, } if (!push_server(serial)) { - SDL_free(server->serial); - return false; + goto error1; } if (!enable_tunnel_any_port(server, params->port_range)) { - SDL_free(server->serial); - return false; + goto error1; } // server will connect to our server socket server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { - if (!server->tunnel_forward) { - close_socket(&server->server_socket); - } - disable_tunnel(server); - SDL_free(server->serial); - return false; + goto error2; } server->tunnel_enabled = true; return true; + +error2: + if (!server->tunnel_forward) { + close_socket(&server->server_socket); + } + disable_tunnel(server); +error1: + SDL_free(server->serial); + return false; } bool From d421741a83a1d7114a85d286f4bb92c52b20f31b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Mar 2020 23:15:15 +0100 Subject: [PATCH 034/214] Wait server from a separate thread Create a thread just to wait for the server process exit. This paves the way to simply wake up a blocking accept() in a portable way. --- app/src/server.c | 24 +++++++++++++++++++++--- app/src/server.h | 3 +++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 2923c6c7..77978d19 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -332,6 +333,14 @@ server_init(struct server *server) { *server = (struct server) SERVER_INITIALIZER; } +static int +run_wait_server(void *data) { + struct server *server = data; + cmd_simple_wait(server->process, NULL); // ignore exit code + LOGD("Server terminated"); + return 0; +} + bool server_start(struct server *server, const char *serial, const struct server_params *params) { @@ -358,6 +367,16 @@ server_start(struct server *server, const char *serial, goto error2; } + server->wait_server_thread = + SDL_CreateThread(run_wait_server, "wait-server", server); + if (!server->wait_server_thread) { + if (!cmd_terminate(server->process)) { + LOGW("Could not terminate server"); + } + cmd_simple_wait(server->process, NULL); // ignore exit code + goto error2; + } + server->tunnel_enabled = true; return true; @@ -430,13 +449,12 @@ server_stop(struct server *server) { LOGW("Could not terminate server"); } - cmd_simple_wait(server->process, NULL); // ignore exit code - LOGD("Server terminated"); - if (server->tunnel_enabled) { // ignore failure disable_tunnel(server); } + + SDL_WaitThread(server->wait_server_thread, NULL); } void diff --git a/app/src/server.h b/app/src/server.h index cc712b63..050f7f01 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -3,6 +3,7 @@ #include #include +#include #include "config.h" #include "command.h" @@ -12,6 +13,7 @@ struct server { char *serial; process_t process; + SDL_Thread *wait_server_thread; socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; @@ -24,6 +26,7 @@ struct server { #define SERVER_INITIALIZER { \ .serial = NULL, \ .process = PROCESS_NONE, \ + .wait_server_thread = NULL, \ .server_socket = INVALID_SOCKET, \ .video_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \ From a346bb80f4ae85abafa2fc5d261d77c433928bea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Mar 2020 23:56:25 +0100 Subject: [PATCH 035/214] Do not block on accept() if server died The server may die before connecting to the client. In that case, the client was blocked indefinitely (until Ctrl+C) on accept(). To avoid the problem, close the server socket once the server process is dead. --- app/src/server.c | 43 +++++++++++++++++++++++++++++++------------ app/src/server.h | 3 +++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 77978d19..bea6fe98 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -318,14 +318,12 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { } static void -close_socket(socket_t *socket) { - assert(*socket != INVALID_SOCKET); - net_shutdown(*socket, SHUT_RDWR); - if (!net_close(*socket)) { +close_socket(socket_t socket) { + assert(socket != INVALID_SOCKET); + net_shutdown(socket, SHUT_RDWR); + if (!net_close(socket)) { LOGW("Could not close socket"); - return; } - *socket = INVALID_SOCKET; } void @@ -337,6 +335,14 @@ static int run_wait_server(void *data) { struct server *server = data; cmd_simple_wait(server->process, NULL); // ignore exit code + // no need for synchronization, server_socket is initialized before this + // thread was created + if (server->server_socket != INVALID_SOCKET + && SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) { + // On Linux, accept() is unblocked by shutdown(), but on Windows, it is + // unblocked by closesocket(). Therefore, call both (close_socket()). + close_socket(server->server_socket); + } LOGD("Server terminated"); return 0; } @@ -367,6 +373,12 @@ server_start(struct server *server, const char *serial, goto error2; } + // If the server process dies before connecting to the server socket, then + // the client will be stuck forever on accept(). To avoid the problem, we + // must be able to wake up the accept() call when the server dies. To keep + // things simple and multiplatform, just spawn a new thread waiting for the + // server process and calling shutdown()/close() on the server socket if + // necessary to wake up any accept() blocking call. server->wait_server_thread = SDL_CreateThread(run_wait_server, "wait-server", server); if (!server->wait_server_thread) { @@ -383,7 +395,9 @@ server_start(struct server *server, const char *serial, error2: if (!server->tunnel_forward) { - close_socket(&server->server_socket); + // the wait server thread is not started, SDL_AtomicSet() is sufficient + SDL_AtomicSet(&server->server_socket_closed, 1); + close_socket(server->server_socket); } disable_tunnel(server); error1: @@ -406,7 +420,11 @@ server_connect_to(struct server *server) { } // we don't need the server socket anymore - close_socket(&server->server_socket); + if (SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) { + // close it from here + close_socket(server->server_socket); + // otherwise, it is closed by run_wait_server() + } } else { uint32_t attempts = 100; uint32_t delay = 100; // ms @@ -433,14 +451,15 @@ server_connect_to(struct server *server) { void server_stop(struct server *server) { - if (server->server_socket != INVALID_SOCKET) { - close_socket(&server->server_socket); + if (server->server_socket != INVALID_SOCKET + && SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) { + close_socket(server->server_socket); } if (server->video_socket != INVALID_SOCKET) { - close_socket(&server->video_socket); + close_socket(server->video_socket); } if (server->control_socket != INVALID_SOCKET) { - close_socket(&server->control_socket); + close_socket(server->control_socket); } assert(server->process != PROCESS_NONE); diff --git a/app/src/server.h b/app/src/server.h index 050f7f01..79db72e5 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -3,6 +3,7 @@ #include #include +#include #include #include "config.h" @@ -14,6 +15,7 @@ struct server { char *serial; process_t process; SDL_Thread *wait_server_thread; + SDL_atomic_t server_socket_closed; socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; @@ -27,6 +29,7 @@ struct server { .serial = NULL, \ .process = PROCESS_NONE, \ .wait_server_thread = NULL, \ + .server_socket_closed = {0}, \ .server_socket = INVALID_SOCKET, \ .video_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \ From 94e16968693bcb326fcbbd85818589c7f4b8d02f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 29 Mar 2020 00:49:08 +0100 Subject: [PATCH 036/214] Do not warn on terminating the server If the server is already dead, terminating it fails. This is expected. --- app/src/server.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index bea6fe98..b9bb9537 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -382,9 +382,7 @@ server_start(struct server *server, const char *serial, server->wait_server_thread = SDL_CreateThread(run_wait_server, "wait-server", server); if (!server->wait_server_thread) { - if (!cmd_terminate(server->process)) { - LOGW("Could not terminate server"); - } + cmd_terminate(server->process); cmd_simple_wait(server->process, NULL); // ignore exit code goto error2; } @@ -464,9 +462,7 @@ server_stop(struct server *server) { assert(server->process != PROCESS_NONE); - if (!cmd_terminate(server->process)) { - LOGW("Could not terminate server"); - } + cmd_terminate(server->process); if (server->tunnel_enabled) { // ignore failure From bea1c11f8ed74854f15b26428f69c7ed78f0de4e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Apr 2020 11:28:48 +0200 Subject: [PATCH 037/214] Do not log success on failure If calling the private API does not work, an exception is printed. In that case, do not log that the action succeeded. --- .../src/main/java/com/genymobile/scrcpy/Device.java | 12 ++++++++---- .../genymobile/scrcpy/wrappers/ClipboardManager.java | 4 +++- .../genymobile/scrcpy/wrappers/SurfaceControl.java | 4 +++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 64d77de3..cbe294af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -163,8 +163,10 @@ public final class Device { } public void setClipboardText(String text) { - serviceManager.getClipboardManager().setText(text); - Ln.i("Device clipboard set"); + boolean ok = serviceManager.getClipboardManager().setText(text); + if (ok) { + Ln.i("Device clipboard set"); + } } /** @@ -176,8 +178,10 @@ public final class Device { Ln.e("Could not get built-in display"); return; } - SurfaceControl.setDisplayPowerMode(d, mode); - Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + boolean ok = SurfaceControl.setDisplayPowerMode(d, mode); + if (ok) { + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } } /** diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 592bdf6b..ade23032 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -74,13 +74,15 @@ public class ClipboardManager { } } - public void setText(CharSequence text) { + public boolean setText(CharSequence text) { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, manager, clipData); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 227bbc85..8fbb860b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -121,12 +121,14 @@ public final class SurfaceControl { return setDisplayPowerModeMethod; } - public static void setDisplayPowerMode(IBinder displayToken, int mode) { + public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { try { Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; } } From 54ccccd8839f16cc289f98160882b909ea7aa3c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Apr 2020 19:16:33 +0200 Subject: [PATCH 038/214] Replace SDL_Atomic by stdatomic from C11 There is no reason to use SDL atomics. --- app/src/fps_counter.c | 26 ++++++++++++++++++-------- app/src/fps_counter.h | 4 ++-- app/src/server.c | 13 ++++++++----- app/src/server.h | 6 +++--- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 58c62d55..b4dd8b9b 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -23,7 +23,7 @@ fps_counter_init(struct fps_counter *counter) { } counter->thread = NULL; - SDL_AtomicSet(&counter->started, 0); + atomic_init(&counter->started, 0); // no need to initialize the other fields, they are unused until started return true; @@ -35,6 +35,16 @@ fps_counter_destroy(struct fps_counter *counter) { SDL_DestroyMutex(counter->mutex); } +static inline bool +is_started(struct fps_counter *counter) { + return atomic_load_explicit(&counter->started, memory_order_acquire); +} + +static inline void +set_started(struct fps_counter *counter, bool started) { + atomic_store_explicit(&counter->started, started, memory_order_release); +} + // must be called with mutex locked static void display_fps(struct fps_counter *counter) { @@ -70,10 +80,10 @@ run_fps_counter(void *data) { mutex_lock(counter->mutex); while (!counter->interrupted) { - while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { + while (!counter->interrupted && !is_started(counter)) { cond_wait(counter->state_cond, counter->mutex); } - while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { + while (!counter->interrupted && is_started(counter)) { uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); @@ -96,7 +106,7 @@ fps_counter_start(struct fps_counter *counter) { counter->nr_skipped = 0; mutex_unlock(counter->mutex); - SDL_AtomicSet(&counter->started, 1); + set_started(counter, true); cond_signal(counter->state_cond); // counter->thread is always accessed from the same thread, no need to lock @@ -114,13 +124,13 @@ fps_counter_start(struct fps_counter *counter) { void fps_counter_stop(struct fps_counter *counter) { - SDL_AtomicSet(&counter->started, 0); + set_started(counter, false); cond_signal(counter->state_cond); } bool fps_counter_is_started(struct fps_counter *counter) { - return SDL_AtomicGet(&counter->started); + return is_started(counter); } void @@ -145,7 +155,7 @@ fps_counter_join(struct fps_counter *counter) { void fps_counter_add_rendered_frame(struct fps_counter *counter) { - if (!SDL_AtomicGet(&counter->started)) { + if (!is_started(counter)) { return; } @@ -158,7 +168,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { void fps_counter_add_skipped_frame(struct fps_counter *counter) { - if (!SDL_AtomicGet(&counter->started)) { + if (!is_started(counter)) { return; } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 1c56bb01..52157172 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -1,9 +1,9 @@ #ifndef FPSCOUNTER_H #define FPSCOUNTER_H +#include #include #include -#include #include #include @@ -16,7 +16,7 @@ struct fps_counter { // atomic so that we can check without locking the mutex // if the FPS counter is disabled, we don't want to lock unnecessarily - SDL_atomic_t started; + atomic_bool started; // the following fields are protected by the mutex bool interrupted; diff --git a/app/src/server.c b/app/src/server.c index b9bb9537..87997e72 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -338,7 +338,7 @@ run_wait_server(void *data) { // no need for synchronization, server_socket is initialized before this // thread was created if (server->server_socket != INVALID_SOCKET - && SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) { + && !atomic_flag_test_and_set(&server->server_socket_closed)) { // On Linux, accept() is unblocked by shutdown(), but on Windows, it is // unblocked by closesocket(). Therefore, call both (close_socket()). close_socket(server->server_socket); @@ -393,8 +393,11 @@ server_start(struct server *server, const char *serial, error2: if (!server->tunnel_forward) { - // the wait server thread is not started, SDL_AtomicSet() is sufficient - SDL_AtomicSet(&server->server_socket_closed, 1); + bool was_closed = + atomic_flag_test_and_set(&server->server_socket_closed); + // the thread is not started, the flag could not be already set + assert(!was_closed); + (void) was_closed; close_socket(server->server_socket); } disable_tunnel(server); @@ -418,7 +421,7 @@ server_connect_to(struct server *server) { } // we don't need the server socket anymore - if (SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) { + if (!atomic_flag_test_and_set(&server->server_socket_closed)) { // close it from here close_socket(server->server_socket); // otherwise, it is closed by run_wait_server() @@ -450,7 +453,7 @@ server_connect_to(struct server *server) { void server_stop(struct server *server) { if (server->server_socket != INVALID_SOCKET - && SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) { + && !atomic_flag_test_and_set(&server->server_socket_closed)) { close_socket(server->server_socket); } if (server->video_socket != INVALID_SOCKET) { diff --git a/app/src/server.h b/app/src/server.h index 79db72e5..a2ecdefc 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,9 +1,9 @@ #ifndef SERVER_H #define SERVER_H +#include #include #include -#include #include #include "config.h" @@ -15,7 +15,7 @@ struct server { char *serial; process_t process; SDL_Thread *wait_server_thread; - SDL_atomic_t server_socket_closed; + atomic_flag server_socket_closed; socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; @@ -29,7 +29,7 @@ struct server { .serial = NULL, \ .process = PROCESS_NONE, \ .wait_server_thread = NULL, \ - .server_socket_closed = {0}, \ + .server_socket_closed = ATOMIC_FLAG_INIT, \ .server_socket = INVALID_SOCKET, \ .video_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \ From 271de0954a15ec23dca945009eb3f576af6abed8 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 3 Apr 2020 20:29:58 +0530 Subject: [PATCH 039/214] Update to AGP 3.6.2 Signed-off-by: Harsh Shandilya --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b6ec625d..94862d2e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:3.6.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 9e78b765da01843b4986f93179b0c92701269062 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 3 Apr 2020 20:30:11 +0530 Subject: [PATCH 040/214] Update to Gradle 6.3 Signed-off-by: Harsh Shandilya Signed-off-by: Romain Vimont -- Note from committer: The binary gradle/wrapper/gradle-wrapper.jar has the expected SHA-256 checksum: $ curl -L https://services.gradle.org/distributions/gradle-6.3-wrapper.jar.sha256 1cef53de8dc192036e7b0cc47584449b0cf570a00d560bfaa6c9eabe06e1fc06 All the changed files match an upgrade executed independently: --- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 58694 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 33 ++++++++++------------- gradlew.bat | 3 +++ 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..490fda8577df6c95960ba7077c43220e5bb2c0d9 100644 GIT binary patch delta 22806 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 +6EP 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 b$#%!p#R9xp*+AlE3}y_kgcumyE`fLe?S@vi delta 19980 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 T<=``BFlIO`$iR?(9ApasgriHy diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 430dfabc..a4b44297 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 8e25e6c1..2fe81a7d 100755 --- a/gradlew +++ b/gradlew @@ -125,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -154,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a14..9109989e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" From d30593e1d53c55cddaf0c597e8827b96c7cd8419 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 3 Apr 2020 20:32:13 +0530 Subject: [PATCH 041/214] gitignore: Add x/ directory People following default build instructions can be caught off guard by seeing the build artifacts in the git tree. Signed-off-by: Harsh Shandilya Signed-off-by: Romain Vimont --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 59bc840d..222769b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ build/ /dist/ .idea/ .gradle/ +/x/ From 2cf022491f39d5d1d8d09330e9c5a5fba73567f4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Apr 2020 18:42:50 +0200 Subject: [PATCH 042/214] Add issue templates Closes #1157 --- .github/ISSUE_TEMPLATE/bug_report.md | 23 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..a295f1b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +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). + +**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...] + +**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). +Format them between code blocks (delimited by ```). +Please do not post screenshots of your terminal, just post the content as text instead. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..524c370f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + + - [ ] I have checked that a similar [feature request](https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22) does not already exist. + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 2afbfc2c751ab2c9c5fdbd68289df54cb61e75da Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Apr 2020 21:27:06 +0200 Subject: [PATCH 043/214] Add Android device and version in issue template --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a295f1b0..29a2bf01 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,6 +14,8 @@ assignees: '' - 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] **Describe the bug** A clear and concise description of what the bug is. From 15e4da08a362d19eb98ff065c29091d25f043805 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Apr 2020 10:37:19 +0200 Subject: [PATCH 044/214] Improve "low quality" section in FAQ --- FAQ.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/FAQ.md b/FAQ.md index 5135cc47..d68a8011 100644 --- a/FAQ.md +++ b/FAQ.md @@ -132,7 +132,7 @@ that's all. See [#37]. ## Client issues -### The quality is low on HiDPI display +### The quality is low On Windows, you may need to configure the [scaling behavior]. @@ -141,8 +141,9 @@ On Windows, you may need to configure the [scaling behavior]. [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 -If your computer definition is far smaller than your screen, then you'll get -poor quality. See [#40]. +If the definition of your client window is far smaller than that of your device +screen, then you'll get poor quality. This is especially visible on text. See +[#40]. [#40]: https://github.com/Genymobile/scrcpy/issues/40 From 9b9e717c41beeba765cdcef7f833f22989c2f614 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Apr 2020 10:38:13 +0200 Subject: [PATCH 045/214] Explain master and dev branches in BUILD People may not guess that `master` is not the development branch. --- BUILD.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/BUILD.md b/BUILD.md index e35d07d0..9f49216e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -8,6 +8,22 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK). [prebuilt server]: #prebuilt-server +## Branches + +### `master` + +The `master` branch concerns the latest release, and is the home page of the +project on Github. + + +### `dev` + +`dev` is the current development branch. Every commit present in `dev` will be +in the next release. + +If you want to contribute code, please base your commits on the latest `dev` +branch. + ## Requirements From cdd8edbbb68d6cecdced5036d8e3a3c8583ac512 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Apr 2020 22:52:14 +0200 Subject: [PATCH 046/214] Add a note about prebuilt server in BUILD.md Mention that it works with a matching client version. --- BUILD.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BUILD.md b/BUILD.md index 9f49216e..8b845583 100644 --- a/BUILD.md +++ b/BUILD.md @@ -263,3 +263,6 @@ meson x --buildtype release --strip -Db_lto=true \ ninja -Cx sudo ninja -Cx install ``` + +The server only works with a matching client version (this server works with the +`master` branch). From fd63e7eb5a60af8e5e52ecfddb9d0f3b7f3740af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2020 12:02:15 +0200 Subject: [PATCH 047/214] Format shortcut documentation For consistency, start the descriptions with a capital letter. --- app/scrcpy.1 | 26 +++++++++++++------------- app/src/cli.c | 40 ++++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index da6c7f2d..60ca3dc3 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -164,15 +164,15 @@ Default is 0 (automatic).\n .TP .B Ctrl+f -switch fullscreen mode +Switch fullscreen mode .TP .B Ctrl+g -resize window to 1:1 (pixel\-perfect) +Resize window to 1:1 (pixel\-perfect) .TP .B Ctrl+x, Double\-click on black borders -resize window to remove black borders +Resize window to remove black borders .TP .B Ctrl+h, Home, Middle\-click @@ -204,43 +204,43 @@ Click on POWER (turn screen on/off) .TP .B Right\-click (when screen is off) -turn screen on +Turn screen on .TP .B Ctrl+o -turn device screen off (keep mirroring) +Turn device screen off (keep mirroring) .TP .B Ctrl+r -rotate device screen +Rotate device screen .TP .B Ctrl+n -expand notification panel +Expand notification panel .TP .B Ctrl+Shift+n -collapse notification panel +Collapse notification panel .TP .B Ctrl+c -copy device clipboard to computer +Copy device clipboard to computer .TP .B Ctrl+v -paste computer clipboard to device +Paste computer clipboard to device .TP .B Ctrl+Shift+v -copy computer clipboard to device +Copy computer clipboard to device .TP .B Ctrl+i -enable/disable FPS counter (print frames/second in logs) +Enable/disable FPS counter (print frames/second in logs) .TP .B Drag & drop APK file -install APK from computer +Install APK from computer .SH Environment variables diff --git a/app/src/cli.c b/app/src/cli.c index bb379f47..142c277f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -143,68 +143,68 @@ scrcpy_print_usage(const char *arg0) { "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" - " switch fullscreen mode\n" + " Switch fullscreen mode\n" "\n" " " CTRL_OR_CMD "+g\n" - " resize window to 1:1 (pixel-perfect)\n" + " Resize window to 1:1 (pixel-perfect)\n" "\n" " " CTRL_OR_CMD "+x\n" " Double-click on black borders\n" - " resize window to remove black borders\n" + " Resize window to remove black borders\n" "\n" " Ctrl+h\n" " Middle-click\n" - " click on HOME\n" + " Click on HOME\n" "\n" " " CTRL_OR_CMD "+b\n" " " CTRL_OR_CMD "+Backspace\n" " Right-click (when screen is on)\n" - " click on BACK\n" + " Click on BACK\n" "\n" " " CTRL_OR_CMD "+s\n" - " click on APP_SWITCH\n" + " Click on APP_SWITCH\n" "\n" " Ctrl+m\n" - " click on MENU\n" + " Click on MENU\n" "\n" " " CTRL_OR_CMD "+Up\n" - " click on VOLUME_UP\n" + " Click on VOLUME_UP\n" "\n" " " CTRL_OR_CMD "+Down\n" - " click on VOLUME_DOWN\n" + " Click on VOLUME_DOWN\n" "\n" " " CTRL_OR_CMD "+p\n" - " click on POWER (turn screen on/off)\n" + " Click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" - " power on\n" + " Power on\n" "\n" " " CTRL_OR_CMD "+o\n" - " turn device screen off (keep mirroring)\n" + " Turn device screen off (keep mirroring)\n" "\n" " " CTRL_OR_CMD "+r\n" - " rotate device screen\n" + " Rotate device screen\n" "\n" " " CTRL_OR_CMD "+n\n" - " expand notification panel\n" + " Expand notification panel\n" "\n" " " CTRL_OR_CMD "+Shift+n\n" - " collapse notification panel\n" + " Collapse notification panel\n" "\n" " " CTRL_OR_CMD "+c\n" - " copy device clipboard to computer\n" + " Copy device clipboard to computer\n" "\n" " " CTRL_OR_CMD "+v\n" - " paste computer clipboard to device\n" + " Paste computer clipboard to device\n" "\n" " " CTRL_OR_CMD "+Shift+v\n" - " copy computer clipboard to device\n" + " Copy computer clipboard to device\n" "\n" " " CTRL_OR_CMD "+i\n" - " enable/disable FPS counter (print frames/second in logs)\n" + " Enable/disable FPS counter (print frames/second in logs)\n" "\n" " Drag & drop APK file\n" - " install APK from computer\n" + " Install APK from computer\n" "\n", arg0, DEFAULT_BIT_RATE, From d48b375a1dbc8bed92e3424b5967e59c2d8f6ca1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Apr 2020 23:03:23 +0200 Subject: [PATCH 048/214] Add shortcuts to rotate display Add Ctrl+Left and Ctrl+Right shortcuts to rotate the display (the content of the scrcpy window). Contrary to --lock-video-orientation, the rotation has no impact on recording, and can be changed dynamically (and immediately). Fixes #218 --- README.md | 2 + app/scrcpy.1 | 8 +++ app/src/cli.c | 6 ++ app/src/input_manager.c | 22 +++++++ app/src/screen.c | 134 ++++++++++++++++++++++++++++++++-------- app/src/screen.h | 7 +++ 6 files changed, 152 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 96b2f087..321acfd6 100644 --- a/README.md +++ b/README.md @@ -491,6 +491,8 @@ Also see [issue #14]. | Action | Shortcut | Shortcut (macOS) | -------------------------------------- |:----------------------------- |:----------------------------- | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` + | Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ + | Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 60ca3dc3..2bcde227 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -166,6 +166,14 @@ Default is 0 (automatic).\n .B Ctrl+f Switch fullscreen mode +.TP +.B Ctrl+Left +Rotate display left + +.TP +.B Ctrl+Right +Rotate display right + .TP .B Ctrl+g Resize window to 1:1 (pixel\-perfect) diff --git a/app/src/cli.c b/app/src/cli.c index 142c277f..c4d5585a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -145,6 +145,12 @@ scrcpy_print_usage(const char *arg0) { " " CTRL_OR_CMD "+f\n" " Switch fullscreen mode\n" "\n" + " " CTRL_OR_CMD "+Left\n" + " Rotate display left\n" + "\n" + " " CTRL_OR_CMD "+Right\n" + " Rotate display right\n" + "\n" " " CTRL_OR_CMD "+g\n" " Resize window to 1:1 (pixel-perfect)\n" "\n" diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 8c4c230a..62fde2fc 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -221,6 +221,18 @@ rotate_device(struct controller *controller) { } } +static void +rotate_client_left(struct screen *screen) { + unsigned new_rotation = (screen->rotation + 1) % 4; + screen_set_rotation(screen, new_rotation); +} + +static void +rotate_client_right(struct screen *screen) { + unsigned new_rotation = (screen->rotation + 3) % 4; + screen_set_rotation(screen, new_rotation); +} + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { @@ -351,6 +363,16 @@ input_manager_process_key(struct input_manager *im, action_volume_up(controller, action); } return; + case SDLK_LEFT: + if (cmd && !shift && down) { + rotate_client_left(im->screen); + } + return; + case SDLK_RIGHT: + if (cmd && !shift && down) { + rotate_client_right(im->screen); + } + return; case SDLK_c: if (control && cmd && !shift && !repeat && down) { request_device_clipboard(controller); diff --git a/app/src/screen.c b/app/src/screen.c index 03e2c3a1..f506e1d6 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -15,6 +15,19 @@ #define DISPLAY_MARGINS 96 +static inline struct size +get_rotated_size(struct size size, int rotation) { + struct size rotated_size; + if (rotation & 1) { + rotated_size.width = size.height; + rotated_size.height = size.width; + } else { + rotated_size.width = size.width; + rotated_size.height = size.height; + } + return rotated_size; +} + // get the window size in a struct size static struct size get_window_size(SDL_Window *window) { @@ -80,8 +93,8 @@ get_preferred_display_bounds(struct size *bounds) { // - it keeps the aspect ratio // - it scales down to make it fit in the display_size static struct size -get_optimal_size(struct size current_size, struct size frame_size) { - if (frame_size.width == 0 || frame_size.height == 0) { +get_optimal_size(struct size current_size, struct size content_size) { + if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; } @@ -100,14 +113,14 @@ get_optimal_size(struct size current_size, struct size frame_size) { h = MIN(current_size.height, display_size.height); } - bool keep_width = frame_size.width * h > frame_size.height * w; + bool keep_width = content_size.width * h > content_size.height * w; if (keep_width) { // remove black borders on top and bottom - h = frame_size.height * w / frame_size.width; + h = content_size.height * w / content_size.width; } else { // remove black borders on left and right (or none at all if it already // fits) - w = frame_size.width * h / frame_size.height; + w = content_size.width * h / content_size.height; } // w and h must fit into 16 bits @@ -117,33 +130,33 @@ get_optimal_size(struct size current_size, struct size frame_size) { // same as get_optimal_size(), but read the current size from the window static inline struct size -get_optimal_window_size(const struct screen *screen, struct size frame_size) { +get_optimal_window_size(const struct screen *screen, struct size content_size) { struct size windowed_size = get_windowed_window_size(screen); - return get_optimal_size(windowed_size, frame_size); + return get_optimal_size(windowed_size, content_size); } // initially, there is no current size, so use the frame size as current size // req_width and req_height, if not 0, are the sizes requested by the user static inline struct size -get_initial_optimal_size(struct size frame_size, uint16_t req_width, +get_initial_optimal_size(struct size content_size, uint16_t req_width, uint16_t req_height) { struct size window_size; if (!req_width && !req_height) { - window_size = get_optimal_size(frame_size, frame_size); + window_size = get_optimal_size(content_size, content_size); } else { if (req_width) { window_size.width = req_width; } else { // compute from the requested height - window_size.width = (uint32_t) req_height * frame_size.width - / frame_size.height; + window_size.width = (uint32_t) req_height * content_size.width + / content_size.height; } if (req_height) { window_size.height = req_height; } else { // compute from the requested width - window_size.height = (uint32_t) req_width * frame_size.height - / frame_size.width; + window_size.height = (uint32_t) req_width * content_size.height + / content_size.width; } } return window_size; @@ -167,9 +180,11 @@ screen_init_rendering(struct screen *screen, const char *window_title, int16_t window_x, int16_t window_y, uint16_t window_width, uint16_t window_height, bool window_borderless) { screen->frame_size = frame_size; + struct size content_size = + get_rotated_size(frame_size, screen->rotation); struct size window_size = - get_initial_optimal_size(frame_size, window_width, window_height); + get_initial_optimal_size(content_size, window_width, window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; #ifdef HIDPI_SUPPORT window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; @@ -206,8 +221,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } - if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width, - frame_size.height)) { + if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width, + content_size.height)) { LOGE("Could not set renderer logical size: %s", SDL_GetError()); screen_destroy(screen); return false; @@ -253,13 +268,51 @@ screen_destroy(struct screen *screen) { } } +void +screen_set_rotation(struct screen *screen, unsigned rotation) { + assert(rotation < 4); + if (rotation == screen->rotation) { + return; + } + + struct size old_content_size = + get_rotated_size(screen->frame_size, screen->rotation); + struct size new_content_size = + get_rotated_size(screen->frame_size, rotation); + + if (SDL_RenderSetLogicalSize(screen->renderer, + new_content_size.width, + new_content_size.height)) { + LOGE("Could not set renderer logical size: %s", SDL_GetError()); + return; + } + + struct size windowed_size = get_windowed_window_size(screen); + struct size target_size = { + .width = (uint32_t) windowed_size.width * new_content_size.width + / old_content_size.width, + .height = (uint32_t) windowed_size.height * new_content_size.height + / old_content_size.height, + }; + target_size = get_optimal_size(target_size, new_content_size); + set_window_size(screen, target_size); + + screen->rotation = rotation; + LOGI("Display rotation set to %u", rotation); + + screen_render(screen); +} + // recreate the texture and resize the window if the frame size has changed static bool prepare_for_frame(struct screen *screen, struct size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { - if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width, - new_frame_size.height)) { + struct size new_content_size = + get_rotated_size(new_frame_size, screen->rotation); + if (SDL_RenderSetLogicalSize(screen->renderer, + new_content_size.width, + new_content_size.height)) { LOGE("Could not set renderer logical size: %s", SDL_GetError()); return false; } @@ -267,14 +320,16 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); + struct size content_size = + get_rotated_size(screen->frame_size, screen->rotation); struct size windowed_size = get_windowed_window_size(screen); struct size target_size = { - (uint32_t) windowed_size.width * new_frame_size.width - / screen->frame_size.width, - (uint32_t) windowed_size.height * new_frame_size.height - / screen->frame_size.height, + (uint32_t) windowed_size.width * new_content_size.width + / content_size.width, + (uint32_t) windowed_size.height * new_content_size.height + / content_size.height, }; - target_size = get_optimal_size(target_size, new_frame_size); + target_size = get_optimal_size(target_size, new_content_size); set_window_size(screen, target_size); screen->frame_size = new_frame_size; @@ -319,7 +374,29 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) { void screen_render(struct screen *screen) { SDL_RenderClear(screen->renderer); - SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + if (screen->rotation == 0) { + SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + } else { + // rotation in RenderCopyEx() is clockwise, while screen->rotation is + // counterclockwise (to be consistent with --lock-video-orientation) + int cw_rotation = (4 - screen->rotation) % 4; + double angle = 90 * cw_rotation; + + SDL_Rect *dstrect = NULL; + SDL_Rect rect; + if (screen->rotation & 1) { + struct size size = + get_rotated_size(screen->frame_size, screen->rotation); + rect.x = (size.width - size.height) / 2; + rect.y = (size.height - size.width) / 2; + rect.w = size.height; + rect.h = size.width; + dstrect = ▭ + } + + SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect, + angle, NULL, 0); + } SDL_RenderPresent(screen->renderer); } @@ -349,8 +426,10 @@ screen_resize_to_fit(struct screen *screen) { screen->maximized = false; } + struct size content_size = + get_rotated_size(screen->frame_size, screen->rotation); struct size optimal_size = - get_optimal_window_size(screen, screen->frame_size); + get_optimal_window_size(screen, content_size); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); LOGD("Resized to optimal size"); } @@ -366,8 +445,9 @@ screen_resize_to_pixel_perfect(struct screen *screen) { screen->maximized = false; } - SDL_SetWindowSize(screen->window, screen->frame_size.width, - screen->frame_size.height); + struct size content_size = + get_rotated_size(screen->frame_size, screen->rotation); + SDL_SetWindowSize(screen->window, content_size.width, content_size.height); LOGD("Resized to pixel-perfect"); } diff --git a/app/src/screen.h b/app/src/screen.h index c31f32c5..4e893811 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -22,6 +22,8 @@ struct screen { // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be // able to revert the size to its non-maximized value. struct size windowed_window_size_backup; + // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) + unsigned rotation; bool has_frame; bool fullscreen; bool maximized; @@ -44,6 +46,7 @@ struct screen { .width = 0, \ .height = 0, \ }, \ + .rotation = 0, \ .has_frame = false, \ .fullscreen = false, \ .maximized = false, \ @@ -90,6 +93,10 @@ screen_resize_to_fit(struct screen *screen); void screen_resize_to_pixel_perfect(struct screen *screen); +// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) +void +screen_set_rotation(struct screen *screen, unsigned rotation); + // react to window events void screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); From 28c71c528f08f961bb0ceacff6d22684635dc3d7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2020 10:17:12 +0200 Subject: [PATCH 049/214] Add --rotation command-line option In addition to Ctrl+Left and Ctrl+Right shortcuts, add a command-line parameter to set the initial rotation. --- README.md | 27 +++++++++++++++++++++++++++ app/scrcpy.1 | 4 ++++ app/src/cli.c | 24 ++++++++++++++++++++++++ app/src/scrcpy.c | 3 ++- app/src/scrcpy.h | 2 ++ app/src/screen.c | 7 ++++++- app/src/screen.h | 3 ++- 7 files changed, 67 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 321acfd6..97be9f71 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,33 @@ scrcpy -f # short version Fullscreen can then be toggled dynamically with `Ctrl`+`f`. +#### Rotation + +The window may be rotated: + +```bash +scrcpy --rotation 1 +``` + +Possibles values are: + - `0`: no rotation + - `1`: 90 degrees counterclockwise + - `2`: 180 degrees + - `3`: 90 degrees clockwise + +The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and +`Ctrl`+`→` _(right)_. + +Note that _scrcpy_ manages 3 different rotations: + - `Ctrl`+`r` requests the device to switch between portrait and landscape (the + current running app may refuse, if it does support the requested + orientation). + - `--lock-video-orientation` changes the mirroring orientation (the orientation + of the video sent from the device to the computer. This affects the + recording. + - `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This + affects only the display, not the recording. + ### Other mirroring options diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2bcde227..e44bfc7e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -110,6 +110,10 @@ Force recording format (either mp4 or mkv). .B \-\-render\-expired\-frames By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. +.TP +.BI "\-\-rotation " value +Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise. + .TP .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. diff --git a/app/src/cli.c b/app/src/cli.c index c4d5585a..44f06ef2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -104,6 +104,11 @@ scrcpy_print_usage(const char *arg0) { " This flag forces to render all frames, at a cost of a\n" " possible increased latency.\n" "\n" + " --rotation value\n" + " Set the initial display rotation.\n" + " Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n" + " degrees rotation counterclockwise.\n" + "\n" " -s, --serial serial\n" " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" @@ -316,6 +321,18 @@ parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) { return true; } +static bool +parse_rotation(const char *s, uint8_t *rotation) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation"); + if (!ok) { + return false; + } + + *rotation = (uint8_t) value; + return true; +} + static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" @@ -435,6 +452,7 @@ guess_record_format(const char *filename) { #define OPT_MAX_FPS 1012 #define OPT_LOCK_VIDEO_ORIENTATION 1013 #define OPT_DISPLAY_ID 1014 +#define OPT_ROTATION 1015 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -455,6 +473,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, + {"rotation", required_argument, NULL, OPT_ROTATION}, {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, @@ -592,6 +611,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_PREFER_TEXT: opts->prefer_text = true; break; + case OPT_ROTATION: + if (!parse_rotation(optarg, &opts->rotation)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 84c87ad8..94e8cde5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -396,7 +396,8 @@ scrcpy(const struct scrcpy_options *options) { options->always_on_top, options->window_x, options->window_y, options->window_width, options->window_height, - options->window_borderless)) { + options->window_borderless, + options->rotation)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index b6c52fd7..93ee6724 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -21,6 +21,7 @@ struct scrcpy_options { uint32_t bit_rate; uint16_t max_fps; int8_t lock_video_orientation; + uint8_t rotation; int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; @@ -52,6 +53,7 @@ struct scrcpy_options { .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ + .rotation = 0, \ .window_x = WINDOW_POSITION_UNDEFINED, \ .window_y = WINDOW_POSITION_UNDEFINED, \ .window_width = 0, \ diff --git a/app/src/screen.c b/app/src/screen.c index f506e1d6..ae33cd27 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -178,8 +178,13 @@ bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless) { + uint16_t window_height, bool window_borderless, + uint8_t rotation) { screen->frame_size = frame_size; + screen->rotation = rotation; + if (rotation) { + LOGI("Initial display rotation set to %u", rotation); + } struct size content_size = get_rotated_size(frame_size, screen->rotation); diff --git a/app/src/screen.h b/app/src/screen.h index 4e893811..8afbbf46 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -63,7 +63,8 @@ bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless); + uint16_t window_height, bool window_borderless, + uint8_t rotation); // show the window void From cbde7b964a7573accf96bb76d2355468d1002fab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2020 12:04:57 +0200 Subject: [PATCH 050/214] Improve documentation for consistency Make --lock-video-orientation documentation consistent with that of --rotation. --- app/scrcpy.1 | 2 +- app/src/cli.c | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e44bfc7e..e3b9f4cc 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -52,7 +52,7 @@ Print this help. .TP .BI "\-\-lock\-video\-orientation " value -Lock video orientation to \fIvalue\fR. Values are integers in the range [-1..3]. Natural device orientation is 0 and each increment adds 90 degrees counterclockwise. +Lock video orientation to \fIvalue\fR. Possible values are -1 (unlocked), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise. Default is -1 (unlocked). diff --git a/app/src/cli.c b/app/src/cli.c index 44f06ef2..0d6ced9b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -52,9 +52,10 @@ scrcpy_print_usage(const char *arg0) { " Print this help.\n" "\n" " --lock-video-orientation value\n" - " Lock video orientation to value. Values are integers in the\n" - " range [-1..3]. Natural device orientation is 0 and each\n" - " increment adds 90 degrees counterclockwise.\n" + " Lock video orientation to value.\n" + " Possible values are -1 (unlocked), 0, 1, 2 and 3.\n" + " Natural device orientation is 0, and each increment adds a\n" + " 90 degrees rotation counterclockwise.\n" " Default is %d%s.\n" "\n" " --max-fps value\n" From a8fd4aec9aea94f86203e3123aa22939d38bb927 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2020 12:06:24 +0200 Subject: [PATCH 051/214] Remove --fullscreen validation Many options are meaningless if --no-display is set. We don't want to validate all possible combinations, so don't make an exception for --fullscreen. --- app/src/cli.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 0d6ced9b..abb7b5f9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -628,11 +628,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } - if (!opts->display && opts->fullscreen) { - LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); - return false; - } - int index = optind; if (index < argc) { LOGE("Unexpected additional argument: %s", argv[index]); From f07d21f050f1151d69f64edd9494cced5b3a1649 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2020 12:08:27 +0200 Subject: [PATCH 052/214] Suppress DiscouragedPrivateApi lint warning --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 2 +- .../java/com/genymobile/scrcpy/wrappers/ServiceManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index fe5d8035..351cc574 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -28,7 +28,7 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi") + @SuppressLint("PrivateApi,DiscouragedPrivateApi") public static void fillAppInfo() { try { // ActivityThread activityThread = new ActivityThread(); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index 0b625c92..cc567837 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -6,7 +6,7 @@ import android.os.IInterface; import java.lang.reflect.Method; -@SuppressLint("PrivateApi") +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { private final Method getServiceMethod; From c1ebea26e6489be65603c1eae1dae6a455351ecf Mon Sep 17 00:00:00 2001 From: Kostiantyn Luzan Date: Tue, 7 Apr 2020 23:48:05 +0300 Subject: [PATCH 053/214] Register rotation watcher on selected display PR #1275 Signed-off-by: Kostiantyn Luzan Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/Device.java | 6 +++--- .../java/com/genymobile/scrcpy/wrappers/WindowManager.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index cbe294af..afa9f165 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -67,7 +67,7 @@ public final class Device { } } } - }); + }, displayId); if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); @@ -138,8 +138,8 @@ public final class Device { return serviceManager.getPowerManager().isScreenOn(); } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { - serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { + serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); } public synchronized void setRotationListener(RotationListener rotationListener) { 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 cc687cd5..faa366a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -93,13 +93,13 @@ public final class WindowManager { } } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { try { Class cls = manager.getClass(); try { // display parameter added since this commit: // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 - cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); From f3fba3c4b968f4242fc0f5d092daa63ffa9b8f5e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2020 14:11:23 +0200 Subject: [PATCH 054/214] Store rotated content size This avoids to compute it every time from the frame size. --- app/src/screen.c | 22 +++++++++------------- app/src/screen.h | 7 ++++++- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index ae33cd27..4dacaae2 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -185,8 +185,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, if (rotation) { LOGI("Initial display rotation set to %u", rotation); } - struct size content_size = - get_rotated_size(frame_size, screen->rotation); + struct size content_size = get_rotated_size(frame_size, screen->rotation); + screen->content_size = content_size; struct size window_size = get_initial_optimal_size(content_size, window_width, window_height); @@ -280,8 +280,7 @@ screen_set_rotation(struct screen *screen, unsigned rotation) { return; } - struct size old_content_size = - get_rotated_size(screen->frame_size, screen->rotation); + struct size old_content_size = screen->content_size; struct size new_content_size = get_rotated_size(screen->frame_size, rotation); @@ -302,6 +301,7 @@ screen_set_rotation(struct screen *screen, unsigned rotation) { target_size = get_optimal_size(target_size, new_content_size); set_window_size(screen, target_size); + screen->content_size = new_content_size; screen->rotation = rotation; LOGI("Display rotation set to %u", rotation); @@ -325,8 +325,7 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); - struct size content_size = - get_rotated_size(screen->frame_size, screen->rotation); + struct size content_size = screen->content_size; struct size windowed_size = get_windowed_window_size(screen); struct size target_size = { (uint32_t) windowed_size.width * new_content_size.width @@ -338,6 +337,7 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { set_window_size(screen, target_size); screen->frame_size = new_frame_size; + screen->content_size = new_content_size; LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); @@ -390,8 +390,7 @@ screen_render(struct screen *screen) { SDL_Rect *dstrect = NULL; SDL_Rect rect; if (screen->rotation & 1) { - struct size size = - get_rotated_size(screen->frame_size, screen->rotation); + struct size size = screen->content_size; rect.x = (size.width - size.height) / 2; rect.y = (size.height - size.width) / 2; rect.w = size.height; @@ -431,10 +430,8 @@ screen_resize_to_fit(struct screen *screen) { screen->maximized = false; } - struct size content_size = - get_rotated_size(screen->frame_size, screen->rotation); struct size optimal_size = - get_optimal_window_size(screen, content_size); + get_optimal_window_size(screen, screen->content_size); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); LOGD("Resized to optimal size"); } @@ -450,8 +447,7 @@ screen_resize_to_pixel_perfect(struct screen *screen) { screen->maximized = false; } - struct size content_size = - get_rotated_size(screen->frame_size, screen->rotation); + struct size content_size = screen->content_size; SDL_SetWindowSize(screen->window, content_size.width, content_size.height); LOGD("Resized to pixel-perfect"); } diff --git a/app/src/screen.h b/app/src/screen.h index 8afbbf46..39593e04 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -17,6 +17,7 @@ struct screen { SDL_Renderer *renderer; SDL_Texture *texture; struct size frame_size; + struct size content_size; // rotated frame_size // The window size the last time it was not maximized or fullscreen. struct size windowed_window_size; // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be @@ -35,7 +36,11 @@ struct screen { .renderer = NULL, \ .texture = NULL, \ .frame_size = { \ - .width = 0, \ + .width = 0, \ + .height = 0, \ + }, \ + .content_size = { \ + .width = 0, \ .height = 0, \ }, \ .windowed_window_size = { \ From 6295c1a1102e22478db58c9119d9590a2cc4c989 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2020 14:27:25 +0200 Subject: [PATCH 055/214] Remap event positions on rotated display If the display is rotated, the position of clicks must be adapted. --- app/src/input_manager.c | 44 +++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 62fde2fc..71a87b57 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -442,6 +442,37 @@ input_manager_process_key(struct input_manager *im, } } +static struct point +rotate_position(struct screen *screen, int32_t x, int32_t y) { + unsigned rotation = screen->rotation; + assert(rotation < 4); + + int32_t w = screen->content_size.width; + int32_t h = screen->content_size.height; + struct point result; + switch (rotation) { + case 0: + result.x = x; + result.y = y; + break; + case 1: + result.x = h - y; + result.y = x; + break; + case 2: + result.x = w - x; + result.y = h - y; + break; + case 3: + result.x = y; + result.y = w - x; + break; + default: + assert(!"Unreachable"); + } + return result; +} + static bool convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, struct control_msg *to) { @@ -449,8 +480,8 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.position.point = + rotate_position(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(from->state); @@ -490,8 +521,9 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = from->fingerId; to->inject_touch_event.position.screen_size = frame_size; // SDL touch event coordinates are normalized in the range [0; 1] - to->inject_touch_event.position.point.x = from->x * frame_size.width; - to->inject_touch_event.position.point.y = from->y * frame_size.height; + float x = from->x * frame_size.width; + float y = from->y * frame_size.height; + to->inject_touch_event.position.point = rotate_position(screen, x, y); to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; return true; @@ -526,8 +558,8 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.position.point = + rotate_position(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); From 9f4735ede3c541163a530c5c474b39ba4696773b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2020 16:34:46 +0200 Subject: [PATCH 056/214] Fix double click on rotated display A double-click outside the device content (in the black borders) resizes so that black borders are removed. But the display rotation was not taken into account to detect the content. Use the content size instead of the frame size to fix the issue. Ref: --- app/src/input_manager.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 71a87b57..39e1252d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -543,8 +543,8 @@ input_manager_process_touch(struct input_manager *im, static bool is_outside_device_screen(struct input_manager *im, int x, int y) { - return x < 0 || x >= im->screen->frame_size.width || - y < 0 || y >= im->screen->frame_size.height; + return x < 0 || x >= im->screen->content_size.width || + y < 0 || y >= im->screen->content_size.height; } static bool From ab52b368958e60d30e68d6c65baa40484eaa9cce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Apr 2020 14:31:14 +0200 Subject: [PATCH 057/214] Reorder options in alphabetical order --- app/src/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index abb7b5f9..488bbc33 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -474,9 +474,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, - {"rotation", required_argument, NULL, OPT_ROTATION}, {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, + {"rotation", required_argument, NULL, OPT_ROTATION}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, From ee2894779aa85a7a78a3e57aa154bfeb1025e55c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Apr 2020 02:05:55 +0200 Subject: [PATCH 058/214] Remove unused lockedVideoOrientation field During PR #1151, this field has been moved to ScreenInfo, but has not been removed from ScreenEncoder. --- .../main/java/com/genymobile/scrcpy/ScreenEncoder.java | 8 +++----- server/src/main/java/com/genymobile/scrcpy/Server.java | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 40457af8..69b309c1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -27,21 +27,19 @@ public class ScreenEncoder implements Device.RotationListener { private int bitRate; private int maxFps; - private int lockedVideoOrientation; private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; - this.lockedVideoOrientation = lockedVideoOrientation; this.iFrameInterval = iFrameInterval; } - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation) { - this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, DEFAULT_I_FRAME_INTERVAL); + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { + this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index bd387285..a6f7a78c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -20,8 +20,7 @@ public final class Server { final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), - options.getLockedVideoOrientation()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); if (options.getControl()) { Controller controller = new Controller(device, connection); From 927d655ff66fa439ccd27e054ae6d54a651640cc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Apr 2020 02:08:56 +0200 Subject: [PATCH 059/214] Simplify ScreenEncoder Do not handle iFrameInterval field and parameter, it is never used dynamically. --- .../main/java/com/genymobile/scrcpy/ScreenEncoder.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 69b309c1..08affaf6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -27,19 +27,13 @@ public class ScreenEncoder implements Device.RotationListener { private int bitRate; private int maxFps; - private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; - this.iFrameInterval = iFrameInterval; - } - - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { - this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -55,7 +49,7 @@ public class ScreenEncoder implements Device.RotationListener { Workarounds.prepareMainLooper(); Workarounds.fillAppInfo(); - MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); + MediaFormat format = createFormat(bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); device.setRotationListener(this); boolean alive; try { From 7eb16ce36458011d87972715f08bd9f03ff39807 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Apr 2020 16:22:43 +0200 Subject: [PATCH 060/214] Fix log format warning The expression port + 1 is promoted to int, but printed as uint16_t. --- app/src/server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 87997e72..b102f0c2 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -172,7 +172,7 @@ enable_tunnel_reverse_any_port(struct server *server, // check before incrementing to avoid overflow on port 65535 if (port < port_range.last) { LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16, - port, port + 1); + port, (uint16_t) (port + 1)); port++; continue; } From ea46d3ab689b92a9ee1c9ef53dc3247feee1adc4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Apr 2020 16:26:55 +0200 Subject: [PATCH 061/214] Add missing include string.h Include for strdup() and strtok_r(). --- app/src/sys/unix/command.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index a60e21bc..64a54e71 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include From 95fa1a69e4985e479486d7e730a59d538a6d70a3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Apr 2020 16:29:12 +0200 Subject: [PATCH 062/214] Workaround compiler warning Some compilers warns on uninitialized value in impossible case: warning: variable 'result' is used uninitialized whenever switch default is taken [-Wsometimes-uninitialized] --- app/src/input_manager.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 39e1252d..589eb842 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -463,12 +463,11 @@ rotate_position(struct screen *screen, int32_t x, int32_t y) { result.x = w - x; result.y = h - y; break; - case 3: + default: + assert(rotation == 3); result.x = y; result.y = w - x; break; - default: - assert(!"Unreachable"); } return result; } From 270d0bf639fbcc8e3a88ff29dc097eb6597a0a88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Apr 2020 19:37:34 +0200 Subject: [PATCH 063/214] Rename max length constant for text injection To avoid confusion with the max text size for clipboard, rename the constant limiting the text injection length. --- app/src/control_msg.c | 5 +++-- app/src/control_msg.h | 2 +- app/tests/test_control_msg_serialize.c | 10 +++++----- .../com/genymobile/scrcpy/ControlMessageReader.java | 2 +- .../genymobile/scrcpy/ControlMessageReaderTest.java | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 45113139..252a3425 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -45,8 +45,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buffer_write32be(&buf[6], msg->inject_keycode.metastate); return 10; case CONTROL_MSG_TYPE_INJECT_TEXT: { - size_t len = write_string(msg->inject_text.text, - CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); + size_t len = + write_string(msg->inject_text.text, + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 49a159a6..e132fc6b 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,7 +10,7 @@ #include "android/keycodes.h" #include "common.h" -#define CONTROL_MSG_TEXT_MAX_LENGTH 300 +#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index d6f556f3..4dc79018 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -49,20 +49,20 @@ static void test_serialize_inject_text(void) { static void test_serialize_inject_text_long(void) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1]; + char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; memset(text, 'a', sizeof(text)); - text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0'; + text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH); + assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH]; + unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; expected[1] = 0x01; expected[2] = 0x2c; // text length (16 bits) - memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH); + memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 29172c08..1c081058 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -13,8 +13,8 @@ public class ControlMessageReader { static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; - public static final int TEXT_MAX_LENGTH = 300; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + public static final int INJECT_TEXT_MAX_LENGTH = 300; private static final int RAW_BUFFER_SIZE = 1024; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 1905942b..202126a5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -66,7 +66,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); - byte[] text = new byte[ControlMessageReader.TEXT_MAX_LENGTH]; + byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; Arrays.fill(text, (byte) 'a'); dos.writeShort(text.length); dos.write(text); From eb8f7a1f288b43e333db12e0f329e7706da5a164 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Apr 2020 22:43:28 +0200 Subject: [PATCH 064/214] Require Meson 0.48 to get rid of warnings Debian buster (stable) provides Meson 0.49, which is also available in stretch (oldstable) backports. It's time to abandon Meson 0.37. Ref: 20b3f101a40cd7455cc5b41e381291504deec5ba --- meson.build | 2 +- server/meson.build | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 412c9c51..e958b20e 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('scrcpy', 'c', version: '1.12.1', - meson_version: '>= 0.37', + meson_version: '>= 0.48', default_options: [ 'c_std=c11', 'warning_level=2', diff --git a/server/meson.build b/server/meson.build index 4ba481d5..984daf3b 100644 --- a/server/meson.build +++ b/server/meson.build @@ -3,7 +3,9 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', - build_always: true, # gradle is responsible for tracking source changes + # gradle is responsible for tracking source changes + build_by_default: true, + build_always_stale: true, output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, From d62eb2b11c4554f3c32a470231355ae0430a956a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Apr 2020 09:57:59 +0200 Subject: [PATCH 065/214] Fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff9a1109..9836d09c 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,7 @@ Note that _scrcpy_ manages 3 different rotations: current running app may refuse, if it does support the requested orientation). - `--lock-video-orientation` changes the mirroring orientation (the orientation - of the video sent from the device to the computer. This affects the + of the video sent from the device to the computer). This affects the recording. - `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This affects only the display, not the recording. From 8a9b20b27e69dc0f77e305e2a0b829d89d264ca7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Apr 2020 14:34:41 +0200 Subject: [PATCH 066/214] Add --render-driver command-line option Add an option to set a render driver hint (SDL_HINT_RENDER_DRIVER). --- app/scrcpy.1 | 8 ++++++++ app/src/cli.c | 12 ++++++++++++ app/src/scrcpy.c | 8 ++++++-- app/src/scrcpy.h | 2 ++ app/src/screen.c | 5 +++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e3b9f4cc..99936732 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -106,6 +106,14 @@ option if set, or by the file extension (.mp4 or .mkv). .BI "\-\-record\-format " format Force recording format (either mp4 or mkv). +.TP +.BI "\-\-render\-driver " name +Request SDL to use the given render driver (this is just a hint). + +Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". +.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER +.UE + .TP .B \-\-render\-expired\-frames By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. diff --git a/app/src/cli.c b/app/src/cli.c index 488bbc33..9c79eb02 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -99,6 +99,13 @@ scrcpy_print_usage(const char *arg0) { " --record-format format\n" " Force recording format (either mp4 or mkv).\n" "\n" + " --render-driver name\n" + " Request SDL to use the given render driver (this is just a\n" + " hint).\n" + " Supported names are currently \"direct3d\", \"opengl\",\n" + " \"opengles2\", \"opengles\", \"metal\" and \"software\".\n" + " \n" + "\n" " --render-expired-frames\n" " By default, to minimize latency, scrcpy always renders the\n" " last available decoded frame, and drops any previous ones.\n" @@ -454,6 +461,7 @@ guess_record_format(const char *filename) { #define OPT_LOCK_VIDEO_ORIENTATION 1013 #define OPT_DISPLAY_ID 1014 #define OPT_ROTATION 1015 +#define OPT_RENDER_DRIVER 1016 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -474,6 +482,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, + {"render-driver", required_argument, NULL, OPT_RENDER_DRIVER}, {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, {"rotation", required_argument, NULL, OPT_ROTATION}, @@ -617,6 +626,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case OPT_RENDER_DRIVER: + opts->render_driver = optarg; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 94e8cde5..0fe3d0f0 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -47,7 +47,7 @@ static struct input_manager input_manager = { // init SDL and set appropriate hints static bool -sdl_init_and_configure(bool display) { +sdl_init_and_configure(bool display, const char *render_driver) { uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; if (SDL_Init(flags)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); @@ -60,6 +60,10 @@ sdl_init_and_configure(bool display) { return true; } + if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) { + LOGW("Could not set render driver"); + } + // Linear filtering if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { LOGW("Could not enable linear filtering"); @@ -310,7 +314,7 @@ scrcpy(const struct scrcpy_options *options) { bool controller_initialized = false; bool controller_started = false; - if (!sdl_init_and_configure(options->display)) { + if (!sdl_init_and_configure(options->display, options->render_driver)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 93ee6724..a53aaa21 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -15,6 +15,7 @@ struct scrcpy_options { const char *record_filename; const char *window_title; const char *push_target; + const char *render_driver; enum recorder_format record_format; struct port_range port_range; uint16_t max_size; @@ -44,6 +45,7 @@ struct scrcpy_options { .record_filename = NULL, \ .window_title = NULL, \ .push_target = NULL, \ + .render_driver = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \ .port_range = { \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ diff --git a/app/src/screen.c b/app/src/screen.c index 4dacaae2..83b07362 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -226,6 +226,11 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } + SDL_RendererInfo renderer_info; + int r = SDL_GetRendererInfo(screen->renderer, &renderer_info); + const char *renderer_name = r ? NULL : renderer_info.name; + LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); + if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width, content_size.height)) { LOGE("Could not set renderer logical size: %s", SDL_GetError()); From bea7658807d276aeab7d18d856a366c83ee05827 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Apr 2020 16:08:23 +0200 Subject: [PATCH 067/214] Enable trilinear filtering for OpenGL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve downscaling quality if mipmapping is available. Suggested-by: Giumo Clanjor (哆啦比猫/兰威举) Fixes #40 Ref: --- app/meson.build | 1 + app/src/opengl.c | 56 ++++++++++++++++++++++++++++++++++++++++++++ app/src/opengl.h | 36 ++++++++++++++++++++++++++++ app/src/screen.c | 61 +++++++++++++++++++++++++++++++++++++++++++----- app/src/screen.h | 7 ++++++ 5 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 app/src/opengl.c create mode 100644 app/src/opengl.h diff --git a/app/meson.build b/app/meson.build index 49c4683f..5d2b4caa 100644 --- a/app/meson.build +++ b/app/meson.build @@ -11,6 +11,7 @@ src = [ 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', + 'src/opengl.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', diff --git a/app/src/opengl.c b/app/src/opengl.c new file mode 100644 index 00000000..da05c082 --- /dev/null +++ b/app/src/opengl.c @@ -0,0 +1,56 @@ +#include "opengl.h" + +#include +#include +#include "SDL2/SDL.h" + +void +sc_opengl_init(struct sc_opengl *gl) { + gl->GetString = SDL_GL_GetProcAddress("glGetString"); + assert(gl->GetString); + + gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf"); + assert(gl->TexParameterf); + + gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri"); + assert(gl->TexParameteri); + + // optional + gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap"); + + const char *version = (const char *) gl->GetString(GL_VERSION); + assert(version); + gl->version = version; + +#define OPENGL_ES_PREFIX "OpenGL ES " + /* starts with "OpenGL ES " */ + gl->is_opengles = !strncmp(gl->version, OPENGL_ES_PREFIX, + sizeof(OPENGL_ES_PREFIX) - 1); + if (gl->is_opengles) { + /* skip the prefix */ + version += sizeof(PREFIX) - 1; + } + + int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor); + if (r != 2) { + // failed to parse the version + gl->version_major = 0; + gl->version_minor = 0; + } +} + +bool +sc_opengl_version_at_least(struct sc_opengl *gl, + int minver_major, int minver_minor, + int minver_es_major, int minver_es_minor) +{ + if (gl->is_opengles) { + return gl->version_major > minver_es_major + || (gl->version_major == minver_es_major + && gl->version_minor >= minver_es_minor); + } + + return gl->version_major > minver_major + || (gl->version_major == minver_major + && gl->version_minor >= minver_minor); +} diff --git a/app/src/opengl.h b/app/src/opengl.h new file mode 100644 index 00000000..f0a89a14 --- /dev/null +++ b/app/src/opengl.h @@ -0,0 +1,36 @@ +#ifndef SC_OPENGL_H +#define SC_OPENGL_H + +#include +#include + +#include "config.h" + +struct sc_opengl { + const char *version; + bool is_opengles; + int version_major; + int version_minor; + + const GLubyte * + (*GetString)(GLenum name); + + void + (*TexParameterf)(GLenum target, GLenum pname, GLfloat param); + + void + (*TexParameteri)(GLenum target, GLenum pname, GLint param); + + void + (*GenerateMipmap)(GLenum target); +}; + +void +sc_opengl_init(struct sc_opengl *gl); + +bool +sc_opengl_version_at_least(struct sc_opengl *gl, + int minver_major, int minver_minor, + int minver_es_major, int minver_es_minor); + +#endif diff --git a/app/src/screen.c b/app/src/screen.c index 83b07362..c982a5c8 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -168,10 +168,30 @@ screen_init(struct screen *screen) { } static inline SDL_Texture * -create_texture(SDL_Renderer *renderer, struct size frame_size) { - return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, - SDL_TEXTUREACCESS_STREAMING, - frame_size.width, frame_size.height); +create_texture(struct screen *screen) { + SDL_Renderer *renderer = screen->renderer; + struct size size = screen->frame_size; + SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, + SDL_TEXTUREACCESS_STREAMING, + size.width, size.height); + if (!texture) { + return NULL; + } + + if (screen->mipmaps) { + struct sc_opengl *gl = &screen->gl; + + SDL_GL_BindTexture(texture, NULL, NULL); + + // Enable trilinear filtering for downscaling + gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -.5f); + + SDL_GL_UnbindTexture(texture); + } + + return texture; } bool @@ -238,6 +258,28 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } + // stats with "opengl" + screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); + if (screen->use_opengl) { + struct sc_opengl *gl = &screen->gl; + sc_opengl_init(gl); + + LOGI("OpenGL version: %s", gl->version); + + bool supports_mipmaps = + sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ + 2, 0 /* OpenGL ES 2.0+ */); + if (supports_mipmaps) { + LOGI("Trilinear filtering enabled"); + screen->mipmaps = true; + } else { + LOGW("Trilinear filtering disabled " + "(OpenGL 3.0+ or ES 2.0+ required)"); + } + } else { + LOGW("Trilinear filtering disabled (not an OpenGL renderer)"); + } + SDL_Surface *icon = read_xpm(icon_xpm); if (icon) { SDL_SetWindowIcon(screen->window, icon); @@ -248,7 +290,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height); - screen->texture = create_texture(screen->renderer, frame_size); + screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); screen_destroy(screen); @@ -346,7 +388,7 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); - screen->texture = create_texture(screen->renderer, new_frame_size); + screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); return false; @@ -363,6 +405,13 @@ update_texture(struct screen *screen, const AVFrame *frame) { frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]); + + if (screen->mipmaps) { + assert(screen->use_opengl); + SDL_GL_BindTexture(screen->texture, NULL, NULL); + screen->gl.GenerateMipmap(GL_TEXTURE_2D); + SDL_GL_UnbindTexture(screen->texture); + } } bool diff --git a/app/src/screen.h b/app/src/screen.h index 39593e04..90537463 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -7,6 +7,7 @@ #include "config.h" #include "common.h" +#include "opengl.h" #define WINDOW_POSITION_UNDEFINED (-0x8000) @@ -16,6 +17,8 @@ struct screen { SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; + bool use_opengl; + struct sc_opengl gl; struct size frame_size; struct size content_size; // rotated frame_size // The window size the last time it was not maximized or fullscreen. @@ -29,12 +32,15 @@ struct screen { bool fullscreen; bool maximized; bool no_window; + bool mipmaps; }; #define SCREEN_INITIALIZER { \ .window = NULL, \ .renderer = NULL, \ .texture = NULL, \ + .use_opengl = false, \ + .gl = {0}, \ .frame_size = { \ .width = 0, \ .height = 0, \ @@ -56,6 +62,7 @@ struct screen { .fullscreen = false, \ .maximized = false, \ .no_window = false, \ + .mipmaps = false, \ } // initialize default values From 11a61b2cb374087884ef1270d2f0b5844d797ffd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Apr 2020 23:55:29 +0200 Subject: [PATCH 068/214] Add option --no-mipmaps Add an option to disable trilinear filtering even if mipmapping is available. --- app/scrcpy.1 | 4 ++++ app/src/cli.c | 10 ++++++++++ app/src/scrcpy.c | 2 +- app/src/scrcpy.h | 2 ++ app/src/screen.c | 22 +++++++++++++--------- app/src/screen.h | 2 +- 6 files changed, 31 insertions(+), 11 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 99936732..673cd11d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -74,6 +74,10 @@ Disable device control (mirror the device in read\-only). .B \-N, \-\-no\-display Do not display device (only when screen recording is enabled). +.TP +.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 .BI "\-p, \-\-port " port[:port] Set the TCP port (range) used by the client to listen. diff --git a/app/src/cli.c b/app/src/cli.c index 9c79eb02..13351dee 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -75,6 +75,11 @@ scrcpy_print_usage(const char *arg0) { " Do not display device (only when screen recording is\n" " enabled).\n" "\n" + " --no-mipmaps\n" + " If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then\n" + " mipmaps are automatically generated to improve downscaling\n" + " quality. This option disables the generation of mipmaps.\n" + "\n" " -p, --port port[:port]\n" " Set the TCP port (range) used by the client to listen.\n" " Default is %d:%d.\n" @@ -462,6 +467,7 @@ guess_record_format(const char *filename) { #define OPT_DISPLAY_ID 1014 #define OPT_ROTATION 1015 #define OPT_RENDER_DRIVER 1016 +#define OPT_NO_MIPMAPS 1017 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -478,6 +484,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"max-size", required_argument, NULL, 'm'}, {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, + {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, {"port", required_argument, NULL, 'p'}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, @@ -629,6 +636,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; + case OPT_NO_MIPMAPS: + opts->mipmaps = false; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0fe3d0f0..81f8239e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -401,7 +401,7 @@ scrcpy(const struct scrcpy_options *options) { options->window_y, options->window_width, options->window_height, options->window_borderless, - options->rotation)) { + options->rotation, options-> mipmaps)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index a53aaa21..d6b0a0f6 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -37,6 +37,7 @@ struct scrcpy_options { bool render_expired_frames; bool prefer_text; bool window_borderless; + bool mipmaps; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -70,6 +71,7 @@ struct scrcpy_options { .render_expired_frames = false, \ .prefer_text = false, \ .window_borderless = false, \ + .mipmaps = true, \ } bool diff --git a/app/src/screen.c b/app/src/screen.c index c982a5c8..512d7bbd 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -199,7 +199,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, uint16_t window_height, bool window_borderless, - uint8_t rotation) { + uint8_t rotation, bool mipmaps) { screen->frame_size = frame_size; screen->rotation = rotation; if (rotation) { @@ -266,15 +266,19 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGI("OpenGL version: %s", gl->version); - bool supports_mipmaps = - sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ - 2, 0 /* OpenGL ES 2.0+ */); - if (supports_mipmaps) { - LOGI("Trilinear filtering enabled"); - screen->mipmaps = true; + if (mipmaps) { + bool supports_mipmaps = + sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ + 2, 0 /* OpenGL ES 2.0+ */); + if (supports_mipmaps) { + LOGI("Trilinear filtering enabled"); + screen->mipmaps = true; + } else { + LOGW("Trilinear filtering disabled " + "(OpenGL 3.0+ or ES 2.0+ required)"); + } } else { - LOGW("Trilinear filtering disabled " - "(OpenGL 3.0+ or ES 2.0+ required)"); + LOGI("Trilinear filtering disabled"); } } else { LOGW("Trilinear filtering disabled (not an OpenGL renderer)"); diff --git a/app/src/screen.h b/app/src/screen.h index 90537463..c9e019a1 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -76,7 +76,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, uint16_t window_height, bool window_borderless, - uint8_t rotation); + uint8_t rotation, bool mipmaps); // show the window void From cc22f4622ae9cfe81eb5dba41da266438de232d8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Apr 2020 00:37:10 +0200 Subject: [PATCH 069/214] Mention mipmapping in FAQ --- FAQ.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/FAQ.md b/FAQ.md index d68a8011..871ae8f0 100644 --- a/FAQ.md +++ b/FAQ.md @@ -134,18 +134,27 @@ that's all. See [#37]. ### The quality is low -On Windows, you may need to configure the [scaling behavior]. +If the definition of your client window is smaller than that of your device +screen, then you might get poor quality, especially visible on text (see [#40]). + +[#40]: https://github.com/Genymobile/scrcpy/issues/40 + +To improve downscaling quality, trilinear filtering is enabled automatically +if the renderer is OpenGL and if it supports mipmapping. + +On Windows, you might want to force OpenGL: + +``` +scrcpy --render-driver=opengl +``` + +You may also need to configure the [scaling behavior]: > `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > > Override high DPI scaling behavior > Scaling performed by: _Application_. [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 -If the definition of your client window is far smaller than that of your device -screen, then you'll get poor quality. This is especially visible on text. See -[#40]. - -[#40]: https://github.com/Genymobile/scrcpy/issues/40 ### KWin compositor crashes From 94a7f1a0f84cb4bc998e7eb117517209e3ca5584 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Apr 2020 21:11:00 +0200 Subject: [PATCH 070/214] Disable input events when necessary Disable input events on secondary displays before Android 10, even if FLAG_PRESENTATION is not set. Refs #1288 --- .../main/java/com/genymobile/scrcpy/Device.java | 17 ++++++----------- .../java/com/genymobile/scrcpy/DisplayInfo.java | 1 - .../scrcpy/wrappers/InputManager.java | 3 +-- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index afa9f165..0c43dd34 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -36,10 +36,7 @@ public final class Device { */ private final int layerStack; - /** - * The FLAG_PRESENTATION from the DisplayInfo - */ - private final boolean isPresentationDisplay; + private final boolean supportsInputEvents; public Device(Options options) { displayId = options.getDisplayId(); @@ -53,7 +50,6 @@ public final class Device { screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); layerStack = displayInfo.getLayerStack(); - isPresentationDisplay = (displayInfoFlags & DisplayInfo.FLAG_PRESENTATION) != 0; registerRotationWatcher(new IRotationWatcher.Stub() { @Override @@ -73,8 +69,10 @@ public final class Device { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } - if (!supportsInputEvents()) { - Ln.w("Input events are not supported for displays with FLAG_PRESENTATION enabled for devices with API lower than 29"); + // main display or any display on Android >= Q + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); } } @@ -116,10 +114,7 @@ public final class Device { } public boolean supportsInputEvents() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return true; - } - return !isPresentationDisplay; + return supportsInputEvents; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java index 50bc94aa..4b8036f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java @@ -8,7 +8,6 @@ public final class DisplayInfo { private final int flags; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; - public static final int FLAG_PRESENTATION = 0x00000008; public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { this.displayId = displayId; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 36d07353..e17b5a17 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -53,8 +53,7 @@ public final class InputManager { method.invoke(inputEvent, displayId); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { - // just a warning, it might happen on old devices - Ln.w("Cannot associate a display id to the input event"); + Ln.e("Cannot associate a display id to the input event", e); return false; } } From 14ead499fd848504895fee7649b5c7430be148b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Apr 2020 18:17:12 +0200 Subject: [PATCH 071/214] Fix touch coordinates on rotated display The touch coordinates were not rotated. --- app/src/input_manager.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 589eb842..72f15ee3 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -515,13 +515,11 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, return false; } - struct size frame_size = screen->frame_size; - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = frame_size; + to->inject_touch_event.position.screen_size = screen->frame_size; // SDL touch event coordinates are normalized in the range [0; 1] - float x = from->x * frame_size.width; - float y = from->y * frame_size.height; + float x = from->x * screen->content_size.width; + float y = from->y * screen->content_size.height; to->inject_touch_event.position.point = rotate_position(screen, x, y); to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; From 125c5561e8f4e169fe52884e47fae1f3b9ffb9f7 Mon Sep 17 00:00:00 2001 From: Tzah Mazuz Date: Sat, 18 Apr 2020 02:13:19 +0200 Subject: [PATCH 072/214] Use MediaFormat constant for MIME type Replace "video/avc" by MIMETYPE_VIDEO_AVC. Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 08affaf6..fc1a25b1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -136,12 +136,12 @@ public class ScreenEncoder implements Device.RotationListener { } private static MediaCodec createCodec() throws IOException { - return MediaCodec.createEncoderByType("video/avc"); + return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); } private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, "video/avc"); + format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // 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); From 44f720e4a4768a11fe2bd9cec8c952341bbbde62 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Apr 2020 00:31:31 +0200 Subject: [PATCH 073/214] Log new size on auto-resize request On "resize to fit" and "resize to pixel-perfect", log the new size. --- app/src/screen.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 512d7bbd..ea1b13f2 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -491,7 +491,8 @@ screen_resize_to_fit(struct screen *screen) { struct size optimal_size = get_optimal_window_size(screen, screen->content_size); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); - LOGD("Resized to optimal size"); + LOGD("Resized to optimal size: %ux%u", optimal_size.width, + optimal_size.height); } void @@ -507,7 +508,8 @@ screen_resize_to_pixel_perfect(struct screen *screen) { struct size content_size = screen->content_size; SDL_SetWindowSize(screen->window, content_size.width, content_size.height); - LOGD("Resized to pixel-perfect"); + LOGD("Resized to pixel-perfect: %ux%u", content_size.width, + content_size.height); } void From 3c9ae99ddad51a5d7acf5867973fc30cef9593c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Apr 2020 18:43:29 +0200 Subject: [PATCH 074/214] Move rotation coordinates to screen Move the window-to-frame coordinates conversion from the input manager to the screen. This will allow to apply more screen-related transformations without impacting the input manager. --- app/src/input_manager.c | 37 ++++--------------------------------- app/src/screen.c | 30 ++++++++++++++++++++++++++++++ app/src/screen.h | 5 +++++ 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 72f15ee3..14ee7e40 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -442,36 +442,6 @@ input_manager_process_key(struct input_manager *im, } } -static struct point -rotate_position(struct screen *screen, int32_t x, int32_t y) { - unsigned rotation = screen->rotation; - assert(rotation < 4); - - int32_t w = screen->content_size.width; - int32_t h = screen->content_size.height; - struct point result; - switch (rotation) { - case 0: - result.x = x; - result.y = y; - break; - case 1: - result.x = h - y; - result.y = x; - break; - case 2: - result.x = w - x; - result.y = h - y; - break; - default: - assert(rotation == 3); - result.x = y; - result.y = w - x; - break; - } - return result; -} - static bool convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, struct control_msg *to) { @@ -480,7 +450,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = - rotate_position(screen, from->x, from->y); + screen_convert_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(from->state); @@ -520,7 +490,8 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, // SDL touch event coordinates are normalized in the range [0; 1] float x = from->x * screen->content_size.width; float y = from->y * screen->content_size.height; - to->inject_touch_event.position.point = rotate_position(screen, x, y); + to->inject_touch_event.position.point = + screen_convert_to_frame_coords(screen, x, y); to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; return true; @@ -556,7 +527,7 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = - rotate_position(screen, from->x, from->y); + screen_convert_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); diff --git a/app/src/screen.c b/app/src/screen.c index ea1b13f2..d4f96a39 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -553,3 +553,33 @@ screen_handle_window_event(struct screen *screen, break; } } + +struct point +screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { + unsigned rotation = screen->rotation; + assert(rotation < 4); + + int32_t w = screen->content_size.width; + int32_t h = screen->content_size.height; + struct point result; + switch (rotation) { + case 0: + result.x = x; + result.y = y; + break; + case 1: + result.x = h - y; + result.y = x; + break; + case 2: + result.x = w - x; + result.y = h - y; + break; + default: + assert(rotation == 3); + result.x = y; + result.y = w - x; + break; + } + return result; +} diff --git a/app/src/screen.h b/app/src/screen.h index c9e019a1..85514279 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -114,4 +114,9 @@ screen_set_rotation(struct screen *screen, unsigned rotation); void screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); +// convert point from window coordinates to frame coordinates +// x and y are expressed in pixels +struct point +screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y); + #endif From 561ede444e7ed2aa4a18a16988d2c8adc9d36151 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Apr 2020 16:36:48 +0200 Subject: [PATCH 075/214] Mention Ubuntu 20.04 package Ubuntu 20.04 has been released today, and scrcpy is available in their repositories: --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 593dd2a9..5506dc31 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ control it using keyboard and mouse. ### Linux -In Debian (_testing_ and _sid_ for now): +On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04): ``` apt install scrcpy From 92cb3a666109b67598cd36b9c0088fb697abbd23 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Apr 2020 21:34:25 +0200 Subject: [PATCH 076/214] Improve resizing workaround Call the same method as when the event is received on the event loop, so that the behavior is the same in both cases. --- app/src/scrcpy.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 81f8239e..d557b208 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -109,9 +109,10 @@ static int event_watcher(void *data, SDL_Event *event) { (void) data; if (event->type == SDL_WINDOWEVENT - && event->window.event == SDL_WINDOWEVENT_RESIZED) { - // called from another thread, not very safe, but it's a workaround! - screen_render(&screen); + && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + // In practice, it seems to always be called from the same thread in + // that specific case. Anyway, it's just a workaround. + screen_handle_window_event(&screen, &event->window); } return 0; } From 8581d6850bb4cf9229e99de188b339da50eee777 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Apr 2020 22:52:02 +0200 Subject: [PATCH 077/214] Stabilize auto-resize The window dimensions are integers, so resizing to fit the content may not be exact. When computing the optimal size, it could cause to reduce alternatively the width and height by few pixels, making the "optimal size" unstable. To avoid this problem, check if the optimal size is already correct either by keeping the width or the height. --- app/src/screen.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index d4f96a39..a9019172 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -113,6 +113,13 @@ get_optimal_size(struct size current_size, struct size content_size) { h = MIN(current_size.height, display_size.height); } + if (h == w * content_size.height / content_size.width + || w == h * content_size.width / content_size.height) { + // The size is already optimal, if we ignore rounding errors due to + // integer window dimensions + return (struct size) {w, h}; + } + bool keep_width = content_size.width * h > content_size.height * w; if (keep_width) { // remove black borders on top and bottom From a14840a5159aa81fb005e41964a2d62ea0d2ae10 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Apr 2020 23:01:58 +0200 Subject: [PATCH 078/214] Fix typo in comments --- app/src/screen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/screen.c b/app/src/screen.c index a9019172..0af8de83 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -265,7 +265,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } - // stats with "opengl" + // starts with "opengl" screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); if (screen->use_opengl) { struct sc_opengl *gl = &screen->gl; From b55ca127f8903f6b27f770b3ff82be4e8337ca93 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Apr 2020 22:56:01 +0200 Subject: [PATCH 079/214] Upgrade FFmpeg (4.2.2) for Windows Include the latest version of FFmpeg in Windows releases. --- Makefile.CrossWindows | 20 ++++++++++---------- cross_win32.txt | 4 ++-- cross_win64.txt | 4 ++-- prebuilt-deps/Makefile | 24 ++++++++++++------------ 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 2b30dcb5..413088c7 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,11 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -115,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/cross_win32.txt b/cross_win32.txt index d13af0e2..17ad376f 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev' prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 09f387e1..faa95d28 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev' prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 892af6c7..0410a787 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \ - 9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \ - ffmpeg-4.2.1-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \ + ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \ + ffmpeg-4.2.2-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \ - c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \ - ffmpeg-4.2.1-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \ + 8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \ + ffmpeg-4.2.2-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \ - 55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \ - ffmpeg-4.2.1-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \ + 5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \ + ffmpeg-4.2.2-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \ - 5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \ - ffmpeg-4.2.1-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \ + f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \ + ffmpeg-4.2.2-win64-dev prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ From 76567e684a9ef1ec2b1577f84f15735335646f9f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Apr 2020 23:00:22 +0200 Subject: [PATCH 080/214] Upgrade SDL (2.0.12) for Windows Include the latest version of SDL in Windows releases. --- Makefile.CrossWindows | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- prebuilt-deps/Makefile | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 413088c7..3aed2019 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -108,7 +108,7 @@ dist-win32: build-server build-win32 build-win32-noconsole cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/SDL2-2.0.10/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -123,7 +123,7 @@ dist-win64: build-server build-win64 build-win64-noconsole cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/SDL2-2.0.10/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ diff --git a/cross_win32.txt b/cross_win32.txt index 17ad376f..7b420690 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index faa95d28..cb9e0954 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 0410a787..088cee92 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -30,9 +30,9 @@ prepare-ffmpeg-dev-win64: ffmpeg-4.2.2-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ - a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \ - SDL2-2.0.10 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ + e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \ + SDL2-2.0.12 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ From 9babe268050cd8a791364e5edb346c3c15a6e575 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Apr 2020 22:24:08 +0200 Subject: [PATCH 081/214] Bump version to 1.13 --- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index e958b20e..49093453 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.12.1', + version: '1.13', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 539a97b8..3fa16519 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 14 - versionName "1.12.1" + versionCode 15 + versionName "1.13" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index c117d89c..06fc0d75 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=1.12.1 +SCRCPY_VERSION_NAME=1.13 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From 4e9e712312c9726824d28bed4fc85721e3a2b203 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Apr 2020 22:56:26 +0200 Subject: [PATCH 082/214] Update links to v1.13 in README and BUILD --- BUILD.md | 6 +++--- README.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BUILD.md b/BUILD.md index 8b845583..e4f175ef 100644 --- a/BUILD.md +++ b/BUILD.md @@ -249,10 +249,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.12.1`][direct-scrcpy-server] - _(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_ + - [`scrcpy-server-v1.13`][direct-scrcpy-server] + _(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 55a380c5..a24e06d0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.12.1) +# scrcpy (v1.13) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -69,10 +69,10 @@ hard). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.12.1.zip`][direct-win64] - _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ + - [`scrcpy-win64-v1.13.zip`][direct-win64] + _(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip It is also available in [Chocolatey]: From d4eeb1c84d67ed590189aa9d083ebd65f75fec92 Mon Sep 17 00:00:00 2001 From: antoninsmetana <52748994+antoninsmetana@users.noreply.github.com> Date: Fri, 1 May 2020 19:18:23 +0200 Subject: [PATCH 083/214] Fix AutoAdb url PR #1344 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a24e06d0..20b777b0 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ You could use [AutoAdb]: autoadb scrcpy -s '{}' ``` -[AutoAdb]: https://github.com/rom1v/usbaudio +[AutoAdb]: https://github.com/rom1v/autoadb #### SSH tunnel From 62c0c1321feec0a673329cb6771efeefb6dc9657 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 Apr 2020 18:37:08 +0200 Subject: [PATCH 084/214] Apply workarounds only on error To avoid NullPointerException on some devices, workarounds have been implemented. But these workaround produce (harmless) internal errors causing exceptions to be printed in the console. To avoid this problem, apply the workarounds only if it fails without them. Fixes #994 Refs #365 Refs #940 --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index fc1a25b1..2377ec02 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -47,8 +47,20 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); - Workarounds.fillAppInfo(); + try { + internalStreamScreen(device, fd); + } catch (NullPointerException e) { + // Retry with workarounds enabled: + // + // + Ln.d("Applying workarounds to avoid NullPointerException"); + Workarounds.fillAppInfo(); + internalStreamScreen(device, fd); + } + } + + private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { MediaFormat format = createFormat(bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); device.setRotationListener(this); boolean alive; From 8c6799297b6c84a21509d5847106494b389d0b97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2019 15:23:16 +0100 Subject: [PATCH 085/214] Implement access to settings without Context Expose methods to access the Android settings using private APIs. This allows to read and write settings values immediately without starting a new process to call "settings". --- .../java/com/genymobile/scrcpy/Device.java | 5 + .../scrcpy/wrappers/ActivityManager.java | 87 ++++++++++++ .../scrcpy/wrappers/ContentProvider.java | 132 ++++++++++++++++++ .../scrcpy/wrappers/ServiceManager.java | 18 +++ 4 files changed, 242 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 0c43dd34..6aa339a1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -199,4 +200,8 @@ public final class Device { wm.thawRotation(); } } + + public ContentProvider createSettingsProvider() { + return serviceManager.getActivityManager().createSettingsProvider(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java new file mode 100644 index 00000000..71967c50 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -0,0 +1,87 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ActivityManager { + + private final IInterface manager; + private Method getContentProviderExternalMethod; + private boolean getContentProviderExternalMethodLegacy; + private Method removeContentProviderExternalMethod; + + public ActivityManager(IInterface manager) { + this.manager = manager; + } + + private Method getGetContentProviderExternalMethod() throws NoSuchMethodException { + if (getContentProviderExternalMethod == null) { + try { + getContentProviderExternalMethod = manager.getClass() + .getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class); + } catch (NoSuchMethodException e) { + // old version + getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); + getContentProviderExternalMethodLegacy = true; + } + } + return getContentProviderExternalMethod; + } + + private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException { + if (removeContentProviderExternalMethod == null) { + removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class); + } + return removeContentProviderExternalMethod; + } + + private ContentProvider getContentProviderExternal(String name, IBinder token) { + try { + Method method = getGetContentProviderExternalMethod(); + Object[] args; + if (!getContentProviderExternalMethodLegacy) { + // new version + args = new Object[]{name, ServiceManager.USER_ID, token, null}; + } else { + // old version + args = new Object[]{name, ServiceManager.USER_ID, token}; + } + // ContentProviderHolder providerHolder = getContentProviderExternal(...); + Object providerHolder = method.invoke(manager, args); + if (providerHolder == null) { + return null; + } + // IContentProvider provider = providerHolder.provider; + Field providerField = providerHolder.getClass().getDeclaredField("provider"); + providerField.setAccessible(true); + Object provider = providerField.get(providerHolder); + if (provider == null) { + return null; + } + return new ContentProvider(this, provider, name, token); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + void removeContentProviderExternal(String name, IBinder token) { + try { + Method method = getRemoveContentProviderExternalMethod(); + method.invoke(manager, name, token); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + + public ContentProvider createSettingsProvider() { + return getContentProviderExternal("settings", new Binder()); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java new file mode 100644 index 00000000..b43494c7 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -0,0 +1,132 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.os.Bundle; +import android.os.IBinder; + +import java.io.Closeable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ContentProvider implements Closeable { + + public static final String TABLE_SYSTEM = "system"; + public static final String TABLE_SECURE = "secure"; + public static final String TABLE_GLOBAL = "global"; + + // See android/providerHolder/Settings.java + private static final String CALL_METHOD_GET_SYSTEM = "GET_system"; + private static final String CALL_METHOD_GET_SECURE = "GET_secure"; + private static final String CALL_METHOD_GET_GLOBAL = "GET_global"; + + private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system"; + private static final String CALL_METHOD_PUT_SECURE = "PUT_secure"; + private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global"; + + private static final String CALL_METHOD_USER_KEY = "_user"; + + private static final String NAME_VALUE_TABLE_VALUE = "value"; + + private final ActivityManager manager; + // android.content.IContentProvider + private final Object provider; + private final String name; + private final IBinder token; + + private Method callMethod; + private boolean callMethodLegacy; + + ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { + this.manager = manager; + this.provider = provider; + this.name = name; + this.token = token; + } + + private Method getCallMethod() throws NoSuchMethodException { + if (callMethod == null) { + try { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + } catch (NoSuchMethodException e) { + // old version + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); + callMethodLegacy = true; + } + } + return callMethod; + } + + private Bundle call(String callMethod, String arg, Bundle extras) { + try { + Method method = getCallMethod(); + Object[] args; + if (!callMethodLegacy) { + args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; + } else { + args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; + } + return (Bundle) method.invoke(provider, args); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + public void close() { + manager.removeContentProviderExternal(name, token); + } + + private static String getGetMethod(String table) { + switch (table) { + case TABLE_SECURE: + return CALL_METHOD_GET_SECURE; + case TABLE_SYSTEM: + return CALL_METHOD_GET_SYSTEM; + case TABLE_GLOBAL: + return CALL_METHOD_GET_GLOBAL; + default: + throw new IllegalArgumentException("Invalid table: " + table); + } + } + + private static String getPutMethod(String table) { + switch (table) { + case TABLE_SECURE: + return CALL_METHOD_PUT_SECURE; + case TABLE_SYSTEM: + return CALL_METHOD_PUT_SYSTEM; + case TABLE_GLOBAL: + return CALL_METHOD_PUT_GLOBAL; + default: + throw new IllegalArgumentException("Invalid table: " + table); + } + } + + public String getValue(String table, String key) { + String method = getGetMethod(table); + Bundle arg = new Bundle(); + arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + Bundle bundle = call(method, key, arg); + if (bundle == null) { + return null; + } + return bundle.getString("value"); + } + + public void putValue(String table, String key, String value) { + String method = getPutMethod(table); + Bundle arg = new Bundle(); + arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + arg.putString(NAME_VALUE_TABLE_VALUE, value); + call(method, key, arg); + } + + public String getAndPutValue(String table, String key, String value) { + String oldValue = getValue(table, key); + if (!value.equals(oldValue)) { + putValue(table, key, value); + } + return oldValue; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index cc567837..f7985813 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -16,6 +16,7 @@ public final class ServiceManager { private PowerManager powerManager; private StatusBarManager statusBarManager; private ClipboardManager clipboardManager; + private ActivityManager activityManager; public ServiceManager() { try { @@ -76,4 +77,21 @@ public final class ServiceManager { } return clipboardManager; } + + public ActivityManager getActivityManager() { + if (activityManager == null) { + try { + // On old Android versions, the ActivityManager is not exposed via AIDL, + // so use ActivityManagerNative.getDefault() + Class cls = Class.forName("android.app.ActivityManagerNative"); + Method getDefaultMethod = cls.getDeclaredMethod("getDefault"); + IInterface am = (IInterface) getDefaultMethod.invoke(null); + activityManager = new ActivityManager(am); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + return activityManager; + } } From 2f74ec2518377103e144684a9fb0c2d358168bbb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 May 2020 00:39:40 +0200 Subject: [PATCH 086/214] Add a clean up process on the device In order to clean up on close, use a separate process which is not killed when the device is disconnected (even if the main process itself is killed). --- .../java/com/genymobile/scrcpy/CleanUp.java | 62 +++++++++++++++++++ .../java/com/genymobile/scrcpy/Server.java | 14 +---- 2 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/CleanUp.java diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java new file mode 100644 index 00000000..ca16cc3d --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -0,0 +1,62 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import java.io.File; +import java.io.IOException; + +/** + * Handle the cleanup of scrcpy, even if the main process is killed. + *

+ * This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process). + */ +public final class CleanUp { + + public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + + private CleanUp() { + // not instantiable + } + + public static void configure() throws IOException { + // TODO + boolean needProcess = false; + if (needProcess) { + startProcess(); + } else { + // There is no additional clean up to do when scrcpy dies + unlinkSelf(); + } + } + + private static void startProcess() throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName()}; + + ProcessBuilder builder = new ProcessBuilder(cmd); + builder.environment().put("CLASSPATH", SERVER_PATH); + builder.start(); + } + + private static void unlinkSelf() { + try { + new File(SERVER_PATH).delete(); + } catch (Exception e) { + Ln.e("Could not unlink server", e); + } + } + + public static void main(String... args) { + unlinkSelf(); + + try { + // Wait for the server to die + System.in.read(); + } catch (IOException e) { + // Expected when the server is dead + } + + Ln.i("Cleaning up"); + // TODO + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a6f7a78c..4ac961de 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -4,12 +4,10 @@ import android.graphics.Rect; import android.media.MediaCodec; import android.os.Build; -import java.io.File; import java.io.IOException; public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; private Server() { // not instantiable @@ -18,6 +16,9 @@ public final class Server { private static void scrcpy(Options options) throws IOException { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); + + CleanUp.configure(); + boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); @@ -132,14 +133,6 @@ public final class Server { return new Rect(x, y, x + width, y + height); } - private static void unlinkSelf() { - try { - new File(SERVER_PATH).delete(); - } catch (Exception e) { - Ln.e("Could not unlink server", e); - } - } - private static void suggestFix(Throwable e) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (e instanceof MediaCodec.CodecException) { @@ -172,7 +165,6 @@ public final class Server { } }); - unlinkSelf(); Options options = createOptions(args); scrcpy(options); } From dbb0df607c988c2a94877ebe61590d021d09bc4e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 May 2020 23:15:48 +0200 Subject: [PATCH 087/214] Move constants to ServiceManager PACKAGE_NAME and USER_ID could be use by several "managers", so move them to the service manager. --- .../genymobile/scrcpy/wrappers/ClipboardManager.java | 12 ++++-------- .../genymobile/scrcpy/wrappers/ServiceManager.java | 4 ++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index ade23032..7a3bfafb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -10,10 +10,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ClipboardManager { - - private static final String PACKAGE_NAME = "com.android.shell"; - private static final int USER_ID = 0; - private final IInterface manager; private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; @@ -46,17 +42,17 @@ public class ClipboardManager { private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - return (ClipData) method.invoke(manager, PACKAGE_NAME); + return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); } - return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID); + return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - method.invoke(manager, clipData, PACKAGE_NAME); + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); } else { - method.invoke(manager, clipData, PACKAGE_NAME, USER_ID); + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index f7985813..c4ce59c2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -8,6 +8,10 @@ import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { + + public static final String PACKAGE_NAME = "com.android.shell"; + public static final int USER_ID = 0; + private final Method getServiceMethod; private WindowManager windowManager; From 4668638ee10764e8341c0770d548c0b74de69a8d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 May 2020 23:49:37 +0200 Subject: [PATCH 088/214] Handle "show touches" on the device-side Now that the server can access the Android settings and clean up properly, handle the "show touches" option from the server. The initial state is now correctly restored, even on device disconnection. --- README.md | 3 +- app/scrcpy.1 | 2 +- app/src/cli.c | 3 +- app/src/scrcpy.c | 39 +------------------ app/src/server.c | 1 + app/src/server.h | 1 + .../java/com/genymobile/scrcpy/CleanUp.java | 22 +++++++---- .../java/com/genymobile/scrcpy/Options.java | 9 +++++ .../java/com/genymobile/scrcpy/Server.java | 21 ++++++++-- 9 files changed, 50 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 20b777b0..ea5731a7 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,8 @@ device). Android provides this feature in _Developers options_. -_Scrcpy_ provides an option to enable this feature on start and disable on exit: +_Scrcpy_ provides an option to enable this feature on start and restore the +initial value on exit: ```bash scrcpy --show-touches diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 673cd11d..15200460 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -136,7 +136,7 @@ Turn the device screen off immediately. .TP .B \-t, \-\-show\-touches -Enable "show touches" on start, disable on quit. +Enable "show touches" on start, restore the initial value on exit.. It only shows physical touches (not clicks from scrcpy). diff --git a/app/src/cli.c b/app/src/cli.c index 13351dee..d6d3c63e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -130,7 +130,8 @@ scrcpy_print_usage(const char *arg0) { " Turn the device screen off immediately.\n" "\n" " -t, --show-touches\n" - " Enable \"show touches\" on start, disable on quit.\n" + " Enable \"show touches\" on start, restore the initial value\n" + " on exit.\n" " It only shows physical touches (not clicks from scrcpy).\n" "\n" " -v, --version\n" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d557b208..015dec08 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -229,21 +229,6 @@ event_loop(bool display, bool control) { return false; } -static process_t -set_show_touches_enabled(const char *serial, bool enabled) { - const char *value = enabled ? "1" : "0"; - const char *const adb_cmd[] = { - "shell", "settings", "put", "system", "show_touches", value - }; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); -} - -static void -wait_show_touches(process_t process) { - // reap the process, ignore the result - process_check_success(process, "show_touches"); -} - static SDL_LogPriority sdl_priority_from_av_level(int level) { switch (level) { @@ -292,19 +277,12 @@ scrcpy(const struct scrcpy_options *options) { .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, + .show_touches = options->show_touches, }; if (!server_start(&server, options->serial, ¶ms)) { return false; } - process_t proc_show_touches = PROCESS_NONE; - bool show_touches_waited; - if (options->show_touches) { - LOGI("Enable show_touches"); - proc_show_touches = set_show_touches_enabled(options->serial, true); - show_touches_waited = false; - } - bool ret = false; bool fps_counter_initialized = false; @@ -421,11 +399,6 @@ scrcpy(const struct scrcpy_options *options) { } } - if (options->show_touches) { - wait_show_touches(proc_show_touches); - show_touches_waited = true; - } - input_manager.prefer_text = options->prefer_text; ret = event_loop(options->display, options->control); @@ -482,16 +455,6 @@ end: fps_counter_destroy(&fps_counter); } - if (options->show_touches) { - if (!show_touches_waited) { - // wait the process which enabled "show touches" - wait_show_touches(proc_show_touches); - } - LOGI("Disable show_touches"); - proc_show_touches = set_show_touches_enabled(options->serial, false); - wait_show_touches(proc_show_touches); - } - server_destroy(&server); return ret; diff --git a/app/src/server.c b/app/src/server.c index b102f0c2..5399d269 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -268,6 +268,7 @@ execute_server(struct server *server, const struct server_params *params) { "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", display_id_string, + params->show_touches ? "true" : "false", }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " diff --git a/app/src/server.h b/app/src/server.h index a2ecdefc..33e99bea 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -51,6 +51,7 @@ struct server_params { int8_t lock_video_orientation; bool control; uint16_t display_id; + bool show_touches; }; // init default values diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ca16cc3d..ccbca275 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -19,19 +19,18 @@ public final class CleanUp { // not instantiable } - public static void configure() throws IOException { - // TODO - boolean needProcess = false; + public static void configure(boolean disableShowTouches) throws IOException { + boolean needProcess = disableShowTouches; if (needProcess) { - startProcess(); + startProcess(disableShowTouches); } else { // There is no additional clean up to do when scrcpy dies unlinkSelf(); } } - private static void startProcess() throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName()}; + private static void startProcess(boolean disableShowTouches) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches)}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", SERVER_PATH); @@ -57,6 +56,15 @@ public final class CleanUp { } Ln.i("Cleaning up"); - // TODO + + boolean disableShowTouches = Boolean.parseBoolean(args[0]); + + if (disableShowTouches) { + ServiceManager serviceManager = new ServiceManager(); + try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) { + Ln.i("Disabling \"show touches\""); + settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0"); + } + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 2a4bba33..838416c8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -12,6 +12,7 @@ public class Options { private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; private int displayId; + private boolean showTouches; public int getMaxSize() { return maxSize; @@ -84,4 +85,12 @@ public class Options { public void setDisplayId(int displayId) { this.displayId = displayId; } + + public boolean getShowTouches() { + return showTouches; + } + + public void setShowTouches(boolean showTouches) { + this.showTouches = showTouches; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4ac961de..43ab0f55 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ContentProvider; + import android.graphics.Rect; import android.media.MediaCodec; import android.os.Build; @@ -17,7 +19,16 @@ public final class Server { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); - CleanUp.configure(); + boolean mustDisableShowTouchesOnCleanUp = false; + if (options.getShowTouches()) { + try (ContentProvider settings = device.createSettingsProvider()) { + String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1"); + // If "show touches" was disabled, it must be disabled back on clean up + mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); + } + } + + CleanUp.configure(mustDisableShowTouchesOnCleanUp); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { @@ -80,8 +91,9 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - if (args.length != 10) { - throw new IllegalArgumentException("Expecting 10 parameters"); + final int expectedParameters = 11; + if (args.length != expectedParameters) { + throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } Options options = new Options(); @@ -114,6 +126,9 @@ public final class Server { int displayId = Integer.parseInt(args[9]); options.setDisplayId(displayId); + boolean showTouches = Boolean.parseBoolean(args[10]); + options.setShowTouches(showTouches); + return options; } From 828327365a51ef918c5b59db3b656feb57c8c9fa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 May 2020 01:51:35 +0200 Subject: [PATCH 089/214] Reorder options in alphabetical order --- app/src/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index d6d3c63e..d49301ac 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -487,6 +487,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"no-display", no_argument, NULL, 'N'}, {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, {"port", required_argument, NULL, 'p'}, + {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, @@ -497,7 +498,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, - {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-x", required_argument, NULL, OPT_WINDOW_X}, From c77024314ddc9fa7cde51828eaef455a1ee10344 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 May 2020 01:54:48 +0200 Subject: [PATCH 090/214] Add an option to keep the device awake Add an option to prevent the device to sleep: scrcpy --stay-awake scrcpy -w The initial state is restored on exit. Fixes #631 --- README.md | 20 +++++++++++ app/scrcpy.1 | 4 +++ app/src/cli.c | 14 +++++++- app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 ++ app/src/server.c | 1 + app/src/server.h | 1 + .../java/com/genymobile/scrcpy/CleanUp.java | 23 ++++++++----- .../java/com/genymobile/scrcpy/Options.java | 9 +++++ .../java/com/genymobile/scrcpy/Server.java | 33 +++++++++++++++---- 10 files changed, 93 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ea5731a7..09cb27b8 100644 --- a/README.md +++ b/README.md @@ -396,6 +396,18 @@ The list of display ids can be retrieved by: adb shell dumpsys display # search "mDisplayId=" in the output ``` +#### Stay awake + +To prevent the device to sleep after some delay: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +The initial state is restored when scrcpy is closed. + + #### Turn screen off It is possible to turn the device screen off while mirroring on start with a @@ -410,6 +422,14 @@ Or by pressing `Ctrl`+`o` at any time. To turn it back on, press `POWER` (or `Ctrl`+`p`). +It can be useful to also prevent the device to sleep: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + + #### Render expired frames By default, to minimize latency, _scrcpy_ always renders the last decoded frame diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 15200460..02389159 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -144,6 +144,10 @@ It only shows physical touches (not clicks from scrcpy). .B \-v, \-\-version Print the version of scrcpy. +.TP +.B \-w, \-\-stay-awake +Keep the device on while scrcpy is running. + .TP .B \-\-window\-borderless Disable window decorations (display borderless window). diff --git a/app/src/cli.c b/app/src/cli.c index d49301ac..0c68279f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -137,6 +137,9 @@ scrcpy_print_usage(const char *arg0) { " -v, --version\n" " Print the version of scrcpy.\n" "\n" + " -w, --stay-awake\n" + " Keep the device on while scrcpy is running.\n" + "\n" " --window-borderless\n" " Disable window decorations (display borderless window).\n" "\n" @@ -497,6 +500,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"rotation", required_argument, NULL, OPT_ROTATION}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, + {"stay-awake", no_argument, NULL, 'w'}, {"turn-screen-off", no_argument, NULL, 'S'}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, @@ -514,7 +518,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { optind = 0; // reset to start from the first argument in tests int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, + while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -594,6 +598,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 'v': args->version = true; break; + case 'w': + opts->stay_awake = true; + break; case OPT_RENDER_EXPIRED_FRAMES: opts->render_expired_frames = true; break; @@ -676,5 +683,10 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } + if (!opts->control && opts->stay_awake) { + LOGE("Could not request to stay awake if control is disabled"); + return false; + } + return true; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 015dec08..c085e769 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -278,6 +278,7 @@ scrcpy(const struct scrcpy_options *options) { .control = options->control, .display_id = options->display_id, .show_touches = options->show_touches, + .stay_awake = options->stay_awake, }; if (!server_start(&server, options->serial, ¶ms)) { return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d6b0a0f6..99e72ef0 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -38,6 +38,7 @@ struct scrcpy_options { bool prefer_text; bool window_borderless; bool mipmaps; + bool stay_awake; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -72,6 +73,7 @@ struct scrcpy_options { .prefer_text = false, \ .window_borderless = false, \ .mipmaps = true, \ + .stay_awake = false, \ } bool diff --git a/app/src/server.c b/app/src/server.c index 5399d269..b98cec2f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -269,6 +269,7 @@ execute_server(struct server *server, const struct server_params *params) { params->control ? "true" : "false", display_id_string, params->show_touches ? "true" : "false", + params->stay_awake ? "true" : "false", }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " diff --git a/app/src/server.h b/app/src/server.h index 33e99bea..13d0b451 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -52,6 +52,7 @@ struct server_params { bool control; uint16_t display_id; bool show_touches; + bool stay_awake; }; // init default values diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ccbca275..74555636 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -19,18 +19,18 @@ public final class CleanUp { // not instantiable } - public static void configure(boolean disableShowTouches) throws IOException { - boolean needProcess = disableShowTouches; + public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException { + boolean needProcess = disableShowTouches || restoreStayOn != -1; if (needProcess) { - startProcess(disableShowTouches); + startProcess(disableShowTouches, restoreStayOn); } else { // There is no additional clean up to do when scrcpy dies unlinkSelf(); } } - private static void startProcess(boolean disableShowTouches) throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches)}; + private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", SERVER_PATH); @@ -58,12 +58,19 @@ public final class CleanUp { Ln.i("Cleaning up"); boolean disableShowTouches = Boolean.parseBoolean(args[0]); + int restoreStayOn = Integer.parseInt(args[1]); - if (disableShowTouches) { + if (disableShowTouches || restoreStayOn != -1) { ServiceManager serviceManager = new ServiceManager(); try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) { - Ln.i("Disabling \"show touches\""); - settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0"); + if (disableShowTouches) { + Ln.i("Disabling \"show touches\""); + settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0"); + } + if (restoreStayOn != -1) { + Ln.i("Restoring \"stay awake\""); + settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn)); + } } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 838416c8..152aa2f7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -13,6 +13,7 @@ public class Options { private boolean control; private int displayId; private boolean showTouches; + private boolean stayAwake; public int getMaxSize() { return maxSize; @@ -93,4 +94,12 @@ public class Options { public void setShowTouches(boolean showTouches) { this.showTouches = showTouches; } + + public boolean getStayAwake() { + return stayAwake; + } + + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 43ab0f55..73776c3e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.wrappers.ContentProvider; import android.graphics.Rect; import android.media.MediaCodec; +import android.os.BatteryManager; import android.os.Build; import java.io.IOException; @@ -20,15 +21,32 @@ public final class Server { final Device device = new Device(options); boolean mustDisableShowTouchesOnCleanUp = false; - if (options.getShowTouches()) { + int restoreStayOn = -1; + if (options.getShowTouches() || options.getStayAwake()) { try (ContentProvider settings = device.createSettingsProvider()) { - String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1"); - // If "show touches" was disabled, it must be disabled back on clean up - mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); + if (options.getShowTouches()) { + String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1"); + // If "show touches" was disabled, it must be disabled back on clean up + mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); + } + + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + try { + restoreStayOn = Integer.parseInt(oldValue); + if (restoreStayOn == stayOn) { + // No need to restore + restoreStayOn = -1; + } + } catch (NumberFormatException e) { + restoreStayOn = 0; + } + } } } - CleanUp.configure(mustDisableShowTouchesOnCleanUp); + CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { @@ -91,7 +109,7 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 11; + final int expectedParameters = 12; if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } @@ -129,6 +147,9 @@ public final class Server { boolean showTouches = Boolean.parseBoolean(args[10]); options.setShowTouches(showTouches); + boolean stayAwake = Boolean.parseBoolean(args[11]); + options.setStayAwake(stayAwake); + return options; } From 74ece9b45b6ee477d786c7a9b6e2f21f90f8b5a2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 5 May 2020 19:01:44 +0200 Subject: [PATCH 091/214] Simplify ScreenEncoder more Commit 927d655ff66fa439ccd27e054ae6d54a651640cc removed the iFrameInternal field and constructor argument. Also remove it from createFormat() arguments. --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 2377ec02..651affde 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -61,7 +61,7 @@ public class ScreenEncoder implements Device.RotationListener { } private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { - MediaFormat format = createFormat(bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); + MediaFormat format = createFormat(bitRate, maxFps); device.setRotationListener(this); boolean alive; try { @@ -151,14 +151,14 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); } - private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { + private static MediaFormat createFormat(int bitRate, int maxFps) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // 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); - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); + 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 if (maxFps > 0) { From ead7ee4a03c9533ea7bb992a9c2df6ec757fd1f0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 6 May 2020 01:05:05 +0200 Subject: [PATCH 092/214] Revert "Improve resizing workaround" This reverts commit 92cb3a666109b67598cd36b9c0088fb697abbd23, which broke the fullscreen/maximized restoration size on Windows. Fixes #1346 --- app/src/scrcpy.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c085e769..096b0794 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -109,10 +109,10 @@ static int event_watcher(void *data, SDL_Event *event) { (void) data; if (event->type == SDL_WINDOWEVENT - && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + && event->window.event == SDL_WINDOWEVENT_RESIZED) { // In practice, it seems to always be called from the same thread in // that specific case. Anyway, it's just a workaround. - screen_handle_window_event(&screen, &event->window); + screen_render(&screen); } return 0; } From 0b4e484da014833f90d58b2b336bf9e5204b1f71 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 6 May 2020 22:21:33 +0200 Subject: [PATCH 093/214] Add a note about multi-display limitation Secondary display mirroring is read-only before Android 10. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 20b777b0..04b644a4 100644 --- a/README.md +++ b/README.md @@ -396,6 +396,10 @@ The list of display ids can be retrieved by: adb shell dumpsys display # search "mDisplayId=" in the output ``` +The secondary display may only be controlled if the device runs at least Android +10 (otherwise it is mirrored in read-only). + + #### Turn screen off It is possible to turn the device screen off while mirroring on start with a From 1a9429f3c73e319f108211a66ad57b8c2c9a60aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 6 May 2020 22:26:43 +0200 Subject: [PATCH 094/214] Improve bug report template Insert a code block to (hopefully) increase the chances that users format their terminal output. --- .github/ISSUE_TEMPLATE/bug_report.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 29a2bf01..1c04da7f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,5 +21,9 @@ assignees: '' A clear and concise description of what the bug is. On errors, please provide the output of the console (and `adb logcat` if relevant). -Format them between code blocks (delimited by ```). + +``` +Please paste terminal output in a code block. +``` + Please do not post screenshots of your terminal, just post the content as text instead. From e2d5f0e7fc98af97f3785ce492b7e8f87b109df1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 May 2020 15:08:11 +0200 Subject: [PATCH 095/214] Send scroll events as a touchscreen Scroll events were sent with a mouse input device. When scrolling on a list, this could cause the whole list to be focused, and drawn with the focus color as background. Send scroll events with a touchscreen input device instead (like motion events). Fixes #1362 --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 32a18e16..eb5a0805 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -215,7 +215,7 @@ public class Controller { MotionEvent event = MotionEvent .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, - InputDevice.SOURCE_MOUSE, 0); + InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } From 28abd98f7fef5161ede934a41287b2c0ec1d7668 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 8 May 2020 14:54:33 +0200 Subject: [PATCH 096/214] Properly handle Ctrl+C on Windows By default, Ctrl+C just kills the process on Windows. This caused corrupted video files on recording. Handle Ctrl+C properly to clean up properly. Fixes #818 --- README.md | 1 - app/src/scrcpy.c | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 09cb27b8..0d0c4c5e 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,6 @@ To disable mirroring while recording: scrcpy --no-display --record file.mp4 scrcpy -Nr file.mkv # interrupt recording with Ctrl+C -# Ctrl+C does not terminate properly on Windows, so disconnect the device ``` "Skipped frames" are recorded, even if they are not displayed in real time (for diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 096b0794..2a4702e6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -7,6 +7,10 @@ #include #include +#ifdef _WIN32 +# include +#endif + #include "config.h" #include "command.h" #include "common.h" @@ -45,6 +49,18 @@ static struct input_manager input_manager = { .prefer_text = false, // initialized later }; +#ifdef _WIN32 +BOOL windows_ctrl_handler(DWORD ctrl_type) { + if (ctrl_type == CTRL_C_EVENT) { + SDL_Event event; + event.type = SDL_QUIT; + SDL_PushEvent(&event); + return TRUE; + } + return FALSE; +} +#endif // _WIN32 + // init SDL and set appropriate hints static bool sdl_init_and_configure(bool display, const char *render_driver) { @@ -56,6 +72,14 @@ sdl_init_and_configure(bool display, const char *render_driver) { atexit(SDL_Quit); +#ifdef _WIN32 + // Clean up properly on Ctrl+C on Windows + bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); + if (!ok) { + LOGW("Could not set Ctrl+C handler"); + } +#endif // _WIN32 + if (!display) { return true; } From 24c803924449b1f72801e982ba35dfb80d54116d Mon Sep 17 00:00:00 2001 From: latreta Date: Wed, 22 Apr 2020 14:07:47 -0300 Subject: [PATCH 097/214] Add Brazilian Portuguese translation Signed-off-by: Romain Vimont --- README.pt-br.md | 529 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 529 insertions(+) create mode 100644 README.pt-br.md diff --git a/README.pt-br.md b/README.pt-br.md new file mode 100644 index 00000000..8fb0f149 --- /dev/null +++ b/README.pt-br.md @@ -0,0 +1,529 @@ +# scrcpy (v1.12.1) + +Esta aplicação fornece visualização e controle de dispositivos Android conectados via +USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso root. +Funciona em _GNU/Linux_, _Windows_ e _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Foco em: + + - **leveza** (Nativo, mostra apenas a tela do dispositivo) + - **performance** (30~60fps) + - **qualidade** (1920×1080 ou acima) + - **baixa latência** ([35~70ms][lowlatency]) + - **baixo tempo de inicialização** (~1 segundo para mostrar a primeira imagem) + - **não intrusivo** (nada é deixado instalado no dispositivo) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Requisitos + +O Dispositivo Android requer pelo menos a API 21 (Android 5.0). + + +Tenha certeza de ter [ativado a depuração USB][enable-adb] no(s) seu(s) dispositivo(s). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + + +Em alguns dispositivos, você também precisará ativar [uma opção adicional][control] para controlá-lo usando o teclado e mouse. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Obtendo o app + + +### Linux + +No Debian (_em testes_ e _sid_ por enquanto): + +``` +apt install scrcpy +``` + +O pacote [Snap] está disponível: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + + +Você também pode [compilar a aplicação manualmente][BUILD] (não se preocupe, não é tão difícil). + + +### Windows + +Para Windows, para simplicidade, um arquivo pré-compilado com todas as dependências +(incluindo `adb`) está disponível: + + - [`scrcpy-win64-v1.12.1.zip`][direct-win64] + _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ + +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip + +Também disponível em [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # se você ainda não o tem +``` + +E no [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # se você ainda não o tem +``` + +[Scoop]: https://scoop.sh + +Você também pode [compilar a aplicação manualmente][BUILD]. + + +### macOS + +A aplicação está disponível em [Homebrew]. Apenas a instale: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Você precisa do `adb`, acessível através do seu `PATH`. Se você ainda não o tem: + +```bash +brew cask install android-platform-tools +``` + +Você também pode [compilar a aplicação manualmente][BUILD]. + + +## Executar + +Plugue um dispositivo Android e execute: + +```bash +scrcpy +``` + +Também aceita argumentos de linha de comando, listados por: + +```bash +scrcpy --help +``` + +## Funcionalidades + +### Configuração de captura + +#### Redução de tamanho + +Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para +aumentar performance. + +Para limitar ambos(largura e altura) para algum valor (ex: 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versão reduzida +``` + +A outra dimensão é calculada para que a proporção do dispositivo seja preservada. +Dessa forma, um dispositivo em 1920x1080 será espelhado em 1024x576. + + +#### Mudanças no bit-rate + +O Padrão de bit-rate é 8 mbps. Para mudar o bitrate do vídeo (ex: para 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versão reduzida +``` + +#### Limitar frame rates + +Em dispositivos com Android >= 10, a captura de frame rate pode ser limitada: + +```bash +scrcpy --max-fps 15 +``` + +#### Cortar + +A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela. + +Isso é útil por exemplo, ao espelhar apenas um olho do Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 no deslocamento (0,0) +``` + +Se `--max-size` também for especificado, redimensionar é aplicado após os cortes. + + +### Gravando + +É possível gravar a tela enquanto ocorre o espelhamento: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Para desativar o espelhamento durante a gravação: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# interrompe a gravação com Ctrl+C +# Ctrl+C não encerrar propriamente no Windows, então desconecte o dispositivo +``` + +"Frames pulados" são gravados, mesmo que não sejam mostrado em tempo real (por motivos de performance). +Frames tem seu _horário_ _carimbado_ no dispositivo, então [Variação de atraso nos pacotes] não impacta na gravação do arquivo. + +[Variação de atraso de pacote]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Conexão + +#### Wireless/Sem fio + +_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se] à um dispositivo via TCP/IP: + +1. Conecte o dispositivo a mesma rede Wi-Fi do seu computador. +2. Pegue o endereço de IP do seu dispositivo (Em Configurações → Sobre o Telefone → Status). +3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`. +4. Desplugue seu dispositivo. +5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua o `DEVICE_IP`)_. +6. Execute `scrcpy` como de costume. + +Pode ser útil diminuir o bit-rate e a resolução: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versão reduzida +``` + +[conectar-se]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### N-dispositivos + +Se alguns dispositivos estão listados em `adb devices`, você precisa especificar o _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versão reduzida +``` + +Se o dispositivo está conectado via TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versão reduzida +``` + +Você pode iniciar algumas instâncias do _scrcpy_ para alguns dispositivos. + +#### Conexão via SSH + +Para conectar-se à um dispositivo remoto, é possível se conectar um cliente local `adb` à um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo _adb_): + +```bash +adb kill-server # encerra o servidor local na 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# mantém isso aberto +``` + +De outro terminal: + +```bash +scrcpy +``` + +Igual para conexões sem fio, pode ser útil reduzir a qualidade: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Configurações de Janela + +#### Título + +Por padrão, o título da janela é o modelo do dispositivo. Isto pode ser mudado: + +```bash +scrcpy --window-title 'Meu dispositivo' +``` + +#### Posição e tamanho + +A posição e tamanho iniciais da janela podem ser especificados: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Sem bordas + +Para desativar decorações da janela: + +```bash +scrcpy --window-borderless +``` + +#### Sempre visível + +Para manter a janela do scrcpy sempre visível: + +```bash +scrcpy --always-on-top +``` + +#### Tela cheia + +A aplicação pode ser iniciada diretamente em tela cheia: + +```bash +scrcpy --fullscreen +scrcpy -f # versão reduzida +``` + +Tela cheia pode ser alternada dinamicamente com `Ctrl`+`f`. + + +### Outras opções de espelhamento + +#### Apenas leitura + +Para desativar controles (tudo que possa interagir com o dispositivo: teclas de entrada, eventos de mouse, arrastar e soltar arquivos): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Desligar a tela + +É possível desligar a tela do dispositivo durante o início do espelhamento com uma opção de linha de comando: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Ou apertando `Ctrl`+`o` durante qualquer momento. + +Para ligar novamente, pressione `POWER` (ou `Ctrl`+`p`). + +#### Frames expirados de renderização + +Por padrão, para minimizar a latência, _scrcpy_ sempre renderiza o último frame decodificado disponível e descarta o anterior. + +Para forçar a renderização de todos os frames ( com o custo de aumento de latência), use: + +```bash +scrcpy --render-expired-frames +``` + +#### Mostrar toques + +Para apresentações, pode ser útil mostrar toques físicos(dispositivo físico). + +Android fornece esta funcionalidade nas _Opções do Desenvolvedor_. + +_Scrcpy_ fornece esta opção de ativar esta funcionalidade no início e desativar no encerramento: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo). + + +### Controle de entrada + +#### Rotacionar a tela do dispositivo + +Pressione `Ctrl`+`r` para mudar entre os modos Retrato e Paisagem. + +Note que só será rotacionado se a aplicação em primeiro plano tiver suporte para o modo requisitado. + +#### Copiar-Colar + +É possível sincronizar áreas de transferência entre computador e o dispositivo, +para ambas direções: + + - `Ctrl`+`c` copia a área de transferência do dispositivo para a área de trasferência do computador; + - `Ctrl`+`Shift`+`v` copia a área de transferência do computador para a área de transferência do dispositivo; + - `Ctrl`+`v` _cola_ a área de transferência do computador como uma sequência de eventos de texto (mas + quebra caracteres não-ASCII). + +#### Preferências de injeção de texto + +Existe dois tipos de [eventos][textevents] gerados ao digitar um texto: + - _eventos de teclas_, sinalizando que a tecla foi pressionada ou solta; + - _eventos de texto_, sinalizando que o texto foi inserido. + +Por padrão, letras são injetadas usando eventos de teclas, assim teclados comportam-se +como esperado em jogos (normalmente para tecladas WASD) + +Mas isto pode [causar problemas][prefertext]. Se você encontrar tal problema, +pode evitá-lo usando: + +```bash +scrcpy --prefer-text +``` + +(mas isto vai quebrar o comportamento do teclado em jogos) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +### Transferência de arquivo + +#### Instalar APK + +Para instalar um APK, arraste e solte o arquivo APK(com extensão `.apk`) na janela _scrcpy_. + +Não existe feedback visual, um log é imprimido no console. + + +#### Enviar arquivo para o dispositivo + +Para enviar um arquivo para o diretório `/sdcard/` no dispositivo, arraste e solte um arquivo não APK para a janela do +_scrcpy_. + +Não existe feedback visual, um log é imprimido no console. + +O diretório alvo pode ser mudado ao iniciar: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### Encaminhamento de áudio + +Áudio não é encaminhando pelo _scrcpy_. Use [USBaudio] (Apenas linux). + +Também veja [issue #14]. + +[USBaudio]: https://github.com/rom1v/usbaudio +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Atalhos + + | Ação | Atalho | Atalho (macOS) + | ------------------------------------------------------------- |:------------------------------- |:----------------------------- + | Alternar para modo de tela cheia | `Ctrl`+`f` | `Cmd`+`f` + | Redimensionar janela para pixel-perfect(Escala 1:1) | `Ctrl`+`g` | `Cmd`+`g` + | Redimensionar janela para tirar as bordas pretas | `Ctrl`+`x` \| _Clique-duplo¹_ | `Cmd`+`x` \| _Clique-duplo¹_ + | Clicar em `HOME` | `Ctrl`+`h` \| _Clique-central_ | `Ctrl`+`h` \| _Clique-central_ + | Clicar em `BACK` | `Ctrl`+`b` \| _Clique-direito²_ | `Cmd`+`b` \| _Clique-direito²_ + | Clicar em `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` + | Clicar em `MENU` | `Ctrl`+`m` | `Ctrl`+`m` + | Clicar em `VOLUME_UP` | `Ctrl`+`↑` _(cima)_ | `Cmd`+`↑` _(cima)_ + | Clicar em `VOLUME_DOWN` | `Ctrl`+`↓` _(baixo)_ | `Cmd`+`↓` _(baixo)_ + | Clicar em `POWER` | `Ctrl`+`p` | `Cmd`+`p` + | Ligar | _Clique-direito²_ | _Clique-direito²_ + | Desligar a tela do dispositivo | `Ctrl`+`o` | `Cmd`+`o` + | Rotacionar tela do dispositivo | `Ctrl`+`r` | `Cmd`+`r` + | Expandir painel de notificação | `Ctrl`+`n` | `Cmd`+`n` + | Esconder painel de notificação | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | Copiar área de transferência do dispositivo para o computador | `Ctrl`+`c` | `Cmd`+`c` + | Colar área de transferência do computador para o dispositivo | `Ctrl`+`v` | `Cmd`+`v` + | Copiar área de transferência do computador para dispositivo | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + | Ativar/desativar contador de FPS(Frames por segundo) | `Ctrl`+`i` | `Cmd`+`i` + +_¹Clique-duplo em bordas pretas para removê-las._ +_²Botão direito liga a tela se ela estiver desligada, clique BACK para o contrário._ + + +## Caminhos personalizados + +Para usar um binário específico _adb_, configure seu caminho na variável de ambiente `ADB`: + + ADB=/caminho/para/adb scrcpy + +Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em +`SCRCPY_SERVER_PATH`. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## Por quê _scrcpy_? + +Um colega me desafiou a encontrar um nome impronunciável como [gnirehtet]. + +[`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## Como compilar? + +Veja [BUILD]. + +[BUILD]: BUILD.md + + +## Problemas comuns + +Veja [FAQ](FAQ.md). + + +## Desenvolvedores + +Leia a [developers page]. + +[developers page]: DEVELOP.md + + +## Licença + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2020 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## Artigos + +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ From 82446b6b0db284e9f9a125cd3e4785586f6845f3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 May 2020 11:47:56 +0200 Subject: [PATCH 098/214] Reference README.md from localized versions Mention that the README.md is the only one being up-to-date. --- README.ko.md | 2 ++ README.pt-br.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.ko.md b/README.ko.md index b232accd..4e6d8fc5 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,3 +1,5 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + # scrcpy (v1.11) This document will be updated frequently along with the original Readme file diff --git a/README.pt-br.md b/README.pt-br.md index 8fb0f149..654f62cb 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -1,3 +1,5 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + # scrcpy (v1.12.1) Esta aplicação fornece visualização e controle de dispositivos Android conectados via From a85848a5415b05b712f4320d7a73afd229f5d43a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 11 May 2020 01:32:54 +0200 Subject: [PATCH 099/214] Fix Windows Ctrl Handler declaration The handler function signature must include the calling convention declaration. Ref: --- app/src/scrcpy.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2a4702e6..e56ad84d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -50,7 +50,7 @@ static struct input_manager input_manager = { }; #ifdef _WIN32 -BOOL windows_ctrl_handler(DWORD ctrl_type) { +BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { SDL_Event event; event.type = SDL_QUIT; From 2608b1dc62d4cce830880bc504d995db96db0406 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 11 May 2020 02:00:46 +0200 Subject: [PATCH 100/214] Factorize window resize When the content size changes, either on frame size or client rotation changes, the window must be resized. Factorize for both cases. --- app/src/screen.c | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 0af8de83..0ca1343b 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -331,6 +331,21 @@ screen_destroy(struct screen *screen) { } } +static void +set_content_size(struct screen *screen, struct size new_content_size) { + struct size old_content_size = screen->content_size; + struct size windowed_size = get_windowed_window_size(screen); + struct size target_size = { + .width = (uint32_t) windowed_size.width * new_content_size.width + / old_content_size.width, + .height = (uint32_t) windowed_size.height * new_content_size.height + / old_content_size.height, + }; + target_size = get_optimal_size(target_size, new_content_size); + set_window_size(screen, target_size); + screen->content_size = new_content_size; +} + void screen_set_rotation(struct screen *screen, unsigned rotation) { assert(rotation < 4); @@ -338,7 +353,6 @@ screen_set_rotation(struct screen *screen, unsigned rotation) { return; } - struct size old_content_size = screen->content_size; struct size new_content_size = get_rotated_size(screen->frame_size, rotation); @@ -349,17 +363,7 @@ screen_set_rotation(struct screen *screen, unsigned rotation) { return; } - struct size windowed_size = get_windowed_window_size(screen); - struct size target_size = { - .width = (uint32_t) windowed_size.width * new_content_size.width - / old_content_size.width, - .height = (uint32_t) windowed_size.height * new_content_size.height - / old_content_size.height, - }; - target_size = get_optimal_size(target_size, new_content_size); - set_window_size(screen, target_size); - - screen->content_size = new_content_size; + set_content_size(screen, new_content_size); screen->rotation = rotation; LOGI("Display rotation set to %u", rotation); @@ -383,19 +387,8 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); - struct size content_size = screen->content_size; - struct size windowed_size = get_windowed_window_size(screen); - struct size target_size = { - (uint32_t) windowed_size.width * new_content_size.width - / content_size.width, - (uint32_t) windowed_size.height * new_content_size.height - / content_size.height, - }; - target_size = get_optimal_size(target_size, new_content_size); - set_window_size(screen, target_size); - + set_content_size(screen, new_content_size); screen->frame_size = new_frame_size; - screen->content_size = new_content_size; LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); From 6b1da2fcffdc31f83519a81400af548c8d1dd787 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 11 May 2020 03:11:13 +0200 Subject: [PATCH 101/214] Simplify size changes in fullscreen or maximized If the content size changes (due to rotation for example) while the window is maximized or fullscreen, the resize must be applied once fullscreen and maximized are disabled. The previous strategy consisted in storing the windowed size, computing the target size on rotation, and applying it on window restoration. But tracking the windowed size (while ignoring the non-windowed size) was tricky, due to unspecified order of SDL events (e.g. size changes can be notified before "maximized" events), race conditions when reading window flags, different behaviors on different platforms... To simplify the whole resize management, store the old content size (the frame size, possibly rotated) when it changes while the window is maximized or fullscreen, so that the new optimal size can be computed on window restoration. --- app/src/screen.c | 97 ++++++++++++++++++++---------------------------- app/src/screen.h | 18 ++++----- 2 files changed, 48 insertions(+), 67 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 0ca1343b..5c1c0a93 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -30,10 +30,10 @@ get_rotated_size(struct size size, int rotation) { // get the window size in a struct size static struct size -get_window_size(SDL_Window *window) { +get_window_size(const struct screen *screen) { int width; int height; - SDL_GetWindowSize(window, &width, &height); + SDL_GetWindowSize(screen->window, &width, &height); struct size size; size.width = width; @@ -41,31 +41,12 @@ get_window_size(SDL_Window *window) { return size; } -// get the windowed window size -static struct size -get_windowed_window_size(const struct screen *screen) { - if (screen->fullscreen || screen->maximized) { - return screen->windowed_window_size; - } - return get_window_size(screen->window); -} - -// apply the windowed window size if fullscreen and maximized are disabled -static void -apply_windowed_size(struct screen *screen) { - if (!screen->fullscreen && !screen->maximized) { - SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, - screen->windowed_window_size.height); - } -} - // set the window size to be applied when fullscreen is disabled static void set_window_size(struct screen *screen, struct size new_size) { - // setting the window size during fullscreen is implementation defined, - // so apply the resize only after fullscreen is disabled - screen->windowed_window_size = new_size; - apply_windowed_size(screen); + assert(!screen->fullscreen); + assert(!screen->maximized); + SDL_SetWindowSize(screen->window, new_size.width, new_size.height); } // get the preferred display bounds (i.e. the screen bounds with some margins) @@ -138,8 +119,8 @@ get_optimal_size(struct size current_size, struct size content_size) { // same as get_optimal_size(), but read the current size from the window static inline struct size get_optimal_window_size(const struct screen *screen, struct size content_size) { - struct size windowed_size = get_windowed_window_size(screen); - return get_optimal_size(windowed_size, content_size); + struct size window_size = get_window_size(screen); + return get_optimal_size(window_size, content_size); } // initially, there is no current size, so use the frame size as current size @@ -308,8 +289,6 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } - screen->windowed_window_size = window_size; - return true; } @@ -332,20 +311,44 @@ screen_destroy(struct screen *screen) { } static void -set_content_size(struct screen *screen, struct size new_content_size) { - struct size old_content_size = screen->content_size; - struct size windowed_size = get_windowed_window_size(screen); +resize_for_content(struct screen *screen, struct size old_content_size, + struct size new_content_size) { + struct size window_size = get_window_size(screen); struct size target_size = { - .width = (uint32_t) windowed_size.width * new_content_size.width + .width = (uint32_t) window_size.width * new_content_size.width / old_content_size.width, - .height = (uint32_t) windowed_size.height * new_content_size.height + .height = (uint32_t) window_size.height * new_content_size.height / old_content_size.height, }; target_size = get_optimal_size(target_size, new_content_size); set_window_size(screen, target_size); +} + +static void +set_content_size(struct screen *screen, struct size new_content_size) { + if (!screen->fullscreen && !screen->maximized) { + resize_for_content(screen, screen->content_size, new_content_size); + } else if (!screen->resize_pending) { + // Store the windowed size to be able to compute the optimal size once + // fullscreen and maximized are disabled + screen->windowed_content_size = screen->content_size; + screen->resize_pending = true; + } + screen->content_size = new_content_size; } +static void +apply_pending_resize(struct screen *screen) { + assert(!screen->fullscreen); + assert(!screen->maximized); + if (screen->resize_pending) { + resize_for_content(screen, screen->windowed_content_size, + screen->content_size); + screen->resize_pending = false; + } +} + void screen_set_rotation(struct screen *screen, unsigned rotation) { assert(rotation < 4); @@ -471,7 +474,9 @@ screen_switch_fullscreen(struct screen *screen) { } screen->fullscreen = !screen->fullscreen; - apply_windowed_size(screen); + if (!screen->fullscreen && !screen->maximized) { + apply_pending_resize(screen); + } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); screen_render(screen); @@ -520,36 +525,14 @@ screen_handle_window_event(struct screen *screen, screen_render(screen); break; case SDL_WINDOWEVENT_SIZE_CHANGED: - if (!screen->fullscreen && !screen->maximized) { - // Backup the previous size: if we receive the MAXIMIZED event, - // then the new size must be ignored (it's the maximized size). - // We could not rely on the window flags due to race conditions - // (they could be updated asynchronously, at least on X11). - screen->windowed_window_size_backup = - screen->windowed_window_size; - - // Save the windowed size, so that it is available once the - // window is maximized or fullscreen is enabled. - screen->windowed_window_size = get_window_size(screen->window); - } screen_render(screen); break; case SDL_WINDOWEVENT_MAXIMIZED: - // The backup size must be non-nul. - assert(screen->windowed_window_size_backup.width); - assert(screen->windowed_window_size_backup.height); - // Revert the last size, it was updated while screen was maximized. - screen->windowed_window_size = screen->windowed_window_size_backup; -#ifdef DEBUG - // Reset the backup to invalid values to detect unexpected usage - screen->windowed_window_size_backup.width = 0; - screen->windowed_window_size_backup.height = 0; -#endif screen->maximized = true; break; case SDL_WINDOWEVENT_RESTORED: screen->maximized = false; - apply_windowed_size(screen); + apply_pending_resize(screen); break; } } diff --git a/app/src/screen.h b/app/src/screen.h index 85514279..7703b5a2 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -21,11 +21,12 @@ struct screen { struct sc_opengl gl; struct size frame_size; struct size content_size; // rotated frame_size - // The window size the last time it was not maximized or fullscreen. - struct size windowed_window_size; - // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be - // able to revert the size to its non-maximized value. - struct size windowed_window_size_backup; + + bool resize_pending; // resize requested while fullscreen or maximized + // The content size the last time the window was not maximized or + // fullscreen (meaningful only when resize_pending is true) + struct size windowed_content_size; + // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) unsigned rotation; bool has_frame; @@ -49,11 +50,8 @@ struct screen { .width = 0, \ .height = 0, \ }, \ - .windowed_window_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .windowed_window_size_backup = { \ + .resize_pending = false, \ + .windowed_content_size = { \ .width = 0, \ .height = 0, \ }, \ From 4c2e10fd743a3e83eacc8828d5fbb89c87e929a8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 12 May 2020 22:30:37 +0200 Subject: [PATCH 102/214] Workaround maximized+fullscreen on Windows On Windows, in maximized+fullscreen state, disabling fullscreen mode unexpectedly triggers the "restored" then "maximized" events, leaving the window in a weird state (maximized according to the events, but not maximized visually). Moreover, apply_pending_resize() asserts that fullscreen is disabled. To avoid the issue, if fullscreen is set, just ignore the "restored" event. --- app/src/screen.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 5c1c0a93..869ae0e0 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -531,6 +531,13 @@ screen_handle_window_event(struct screen *screen, screen->maximized = true; break; case SDL_WINDOWEVENT_RESTORED: + if (screen->fullscreen) { + // On Windows, in maximized+fullscreen, disabling fullscreen + // mode unexpectedly triggers the "restored" then "maximized" + // events, leaving the window in a weird state (maximized + // according to the events, but not maximized visually). + break; + } screen->maximized = false; apply_pending_resize(screen); break; From ec047b501e830dea136d91dc5e7289077488fe34 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 May 2020 18:53:43 +0200 Subject: [PATCH 103/214] Disable "resize to fit" in maximized state In maximized state (but not fullscreen), it was possible to resize to fit the device screen (with Ctrl+x or double-clicking on black borders). This caused problems on macOS with the "expand to fullscreen" feature, which behaves like a fullscreen mode but is seen as maximized by SDL. In that state, resizing to fit causes unexpected results. To keep the behavior consistent on all platforms, just disable "resize to fit" when the window is maximized. --- app/src/screen.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 869ae0e0..314b47ba 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -484,15 +484,10 @@ screen_switch_fullscreen(struct screen *screen) { void screen_resize_to_fit(struct screen *screen) { - if (screen->fullscreen) { + if (screen->fullscreen || screen->maximized) { return; } - if (screen->maximized) { - SDL_RestoreWindow(screen->window); - screen->maximized = false; - } - struct size optimal_size = get_optimal_window_size(screen, screen->content_size); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); From d860ad48e6be41ec7c7ccbfca8b305fe126fc340 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Apr 2020 00:19:44 +0200 Subject: [PATCH 104/214] Extract optimal window size detection Extract the computation to detect whether the current size of the window is already optimal. This will allow to reuse it for rendering. --- app/src/screen.c | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 314b47ba..566b2d99 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -68,6 +68,16 @@ get_preferred_display_bounds(struct size *bounds) { return true; } +static bool +is_optimal_size(struct size current_size, struct size content_size) { + // The size is optimal if we can recompute one dimension of the current + // size from the other + return current_size.height == current_size.width * content_size.height + / content_size.width + || current_size.width == current_size.height * content_size.width + / content_size.height; +} + // return the optimal size of the window, with the following constraints: // - it attempts to keep at least one dimension of the current_size (i.e. it // crops the black borders) @@ -80,40 +90,36 @@ get_optimal_size(struct size current_size, struct size content_size) { return current_size; } - struct size display_size; - // 32 bits because we need to multiply two 16 bits values - uint32_t w; - uint32_t h; + struct size window_size; + struct size display_size; if (!get_preferred_display_bounds(&display_size)) { // could not get display bounds, do not constraint the size - w = current_size.width; - h = current_size.height; + window_size.width = current_size.width; + window_size.height = current_size.height; } else { - w = MIN(current_size.width, display_size.width); - h = MIN(current_size.height, display_size.height); + window_size.width = MIN(current_size.width, display_size.width); + window_size.height = MIN(current_size.height, display_size.height); } - if (h == w * content_size.height / content_size.width - || w == h * content_size.width / content_size.height) { - // The size is already optimal, if we ignore rounding errors due to - // integer window dimensions - return (struct size) {w, h}; + if (is_optimal_size(window_size, content_size)) { + return window_size; } - bool keep_width = content_size.width * h > content_size.height * w; + bool keep_width = content_size.width * window_size.height + > content_size.height * window_size.width; if (keep_width) { // remove black borders on top and bottom - h = content_size.height * w / content_size.width; + window_size.height = content_size.height * window_size.width + / content_size.width; } else { // remove black borders on left and right (or none at all if it already // fits) - w = content_size.width * h / content_size.height; + window_size.width = content_size.width * window_size.height + / content_size.height; } - // w and h must fit into 16 bits - assert(w < 0x10000 && h < 0x10000); - return (struct size) {w, h}; + return window_size; } // same as get_optimal_size(), but read the current size from the window From e40532a3761da8b3aef484f1d435ef5f50d94574 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Apr 2020 18:44:24 +0200 Subject: [PATCH 105/214] Manually position and scale the content Position and scale the content "manually" instead of relying on the renderer "logical size". This avoids possible rounding differences between the computed window size and the content size, causing one row or column of black pixels on the bottom or on the right. This also avoids HiDPI scale issues, by computing the scaling manually. This will also enable to draw items at their expected size on the screen (unscaled). Fixes #15 --- app/src/input_manager.c | 61 ++++++++------------ app/src/scrcpy.c | 2 +- app/src/screen.c | 119 ++++++++++++++++++++++++++++------------ app/src/screen.h | 20 ++++++- 4 files changed, 125 insertions(+), 77 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 14ee7e40..e8c8d68e 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -7,33 +7,6 @@ #include "util/lock.h" #include "util/log.h" -// Convert window coordinates (as provided by SDL_GetMouseState() to renderer -// coordinates (as provided in SDL mouse events) -// -// See my question: -// -static void -convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) { - SDL_Rect viewport; - float scale_x, scale_y; - SDL_RenderGetViewport(renderer, &viewport); - SDL_RenderGetScale(renderer, &scale_x, &scale_y); - *x = (int) (*x / scale_x) - viewport.x; - *y = (int) (*y / scale_y) - viewport.y; -} - -static struct point -get_mouse_point(struct screen *screen) { - int x; - int y; - SDL_GetMouseState(&x, &y); - convert_to_renderer_coordinates(screen->renderer, &x, &y); - return (struct point) { - .x = x, - .y = y, - }; -} - static const int ACTION_DOWN = 1; static const int ACTION_UP = 1 << 1; @@ -487,11 +460,17 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = from->fingerId; to->inject_touch_event.position.screen_size = screen->frame_size; + + int ww; + int wh; + SDL_GL_GetDrawableSize(screen->window, &ww, &wh); + // SDL touch event coordinates are normalized in the range [0; 1] - float x = from->x * screen->content_size.width; - float y = from->y * screen->content_size.height; + int32_t x = from->x * ww; + int32_t y = from->y * wh; to->inject_touch_event.position.point = screen_convert_to_frame_coords(screen, x, y); + to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; return true; @@ -508,13 +487,6 @@ input_manager_process_touch(struct input_manager *im, } } -static bool -is_outside_device_screen(struct input_manager *im, int x, int y) -{ - return x < 0 || x >= im->screen->content_size.width || - y < 0 || y >= im->screen->content_size.height; -} - static bool convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, struct control_msg *to) { @@ -552,10 +524,15 @@ input_manager_process_mouse_button(struct input_manager *im, action_home(im->controller, ACTION_DOWN | ACTION_UP); return; } + // double-click on black borders resize to fit the device screen if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { - bool outside = - is_outside_device_screen(im, event->x, event->y); + int32_t x = event->x; + int32_t y = event->y; + 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) { screen_resize_to_fit(im->screen); return; @@ -579,9 +556,15 @@ input_manager_process_mouse_button(struct input_manager *im, static bool convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, struct control_msg *to) { + + // mouse_x and mouse_y are expressed in pixels relative to the window + int mouse_x; + int mouse_y; + SDL_GetMouseState(&mouse_x, &mouse_y); + struct position position = { .screen_size = screen->frame_size, - .point = get_mouse_point(screen), + .point = screen_convert_to_frame_coords(screen, mouse_x, mouse_y), }; to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e56ad84d..7f8d3c75 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -136,7 +136,7 @@ event_watcher(void *data, SDL_Event *event) { && event->window.event == SDL_WINDOWEVENT_RESIZED) { // In practice, it seems to always be called from the same thread in // that specific case. Anyway, it's just a workaround. - screen_render(&screen); + screen_render(&screen, true); } return 0; } diff --git a/app/src/screen.c b/app/src/screen.c index 566b2d99..967cf5d7 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -156,6 +156,43 @@ get_initial_optimal_size(struct size content_size, uint16_t req_width, return window_size; } +static void +screen_update_content_rect(struct screen *screen) { + int dw; + int dh; + SDL_GL_GetDrawableSize(screen->window, &dw, &dh); + + struct size content_size = screen->content_size; + // The drawable size is the window size * the HiDPI scale + struct size drawable_size = {dw, dh}; + + SDL_Rect *rect = &screen->rect; + + if (is_optimal_size(drawable_size, content_size)) { + rect->x = 0; + rect->y = 0; + rect->w = drawable_size.width; + rect->h = drawable_size.height; + return; + } + + bool keep_width = content_size.width * drawable_size.height + > content_size.height * drawable_size.width; + if (keep_width) { + rect->x = 0; + rect->w = drawable_size.width; + rect->h = drawable_size.width * content_size.height + / content_size.width; + rect->y = (drawable_size.height - rect->h) / 2; + } else { + rect->y = 0; + rect->h = drawable_size.height; + rect->w = drawable_size.height * content_size.width + / content_size.height; + rect->x = (drawable_size.width - rect->w) / 2; + } +} + void screen_init(struct screen *screen) { *screen = (struct screen) SCREEN_INITIALIZER; @@ -245,13 +282,6 @@ screen_init_rendering(struct screen *screen, const char *window_title, const char *renderer_name = r ? NULL : renderer_info.name; LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); - if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width, - content_size.height)) { - LOGE("Could not set renderer logical size: %s", SDL_GetError()); - screen_destroy(screen); - return false; - } - // starts with "opengl" screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); if (screen->use_opengl) { @@ -295,6 +325,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } + screen_update_content_rect(screen); + return true; } @@ -365,18 +397,12 @@ screen_set_rotation(struct screen *screen, unsigned rotation) { struct size new_content_size = get_rotated_size(screen->frame_size, rotation); - if (SDL_RenderSetLogicalSize(screen->renderer, - new_content_size.width, - new_content_size.height)) { - LOGE("Could not set renderer logical size: %s", SDL_GetError()); - return; - } - set_content_size(screen, new_content_size); + screen->rotation = rotation; LOGI("Display rotation set to %u", rotation); - screen_render(screen); + screen_render(screen, true); } // recreate the texture and resize the window if the frame size has changed @@ -384,21 +410,17 @@ static bool prepare_for_frame(struct screen *screen, struct size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { - struct size new_content_size = - get_rotated_size(new_frame_size, screen->rotation); - if (SDL_RenderSetLogicalSize(screen->renderer, - new_content_size.width, - new_content_size.height)) { - LOGE("Could not set renderer logical size: %s", SDL_GetError()); - return false; - } - // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); - set_content_size(screen, new_content_size); screen->frame_size = new_frame_size; + struct size new_content_size = + get_rotated_size(new_frame_size, screen->rotation); + set_content_size(screen, new_content_size); + + screen_update_content_rect(screen); + LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); screen->texture = create_texture(screen); @@ -439,15 +461,19 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) { update_texture(screen, frame); mutex_unlock(vb->mutex); - screen_render(screen); + screen_render(screen, false); return true; } void -screen_render(struct screen *screen) { +screen_render(struct screen *screen, bool update_content_rect) { + if (update_content_rect) { + screen_update_content_rect(screen); + } + SDL_RenderClear(screen->renderer); if (screen->rotation == 0) { - SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect); } else { // rotation in RenderCopyEx() is clockwise, while screen->rotation is // counterclockwise (to be consistent with --lock-video-orientation) @@ -457,12 +483,14 @@ screen_render(struct screen *screen) { SDL_Rect *dstrect = NULL; SDL_Rect rect; if (screen->rotation & 1) { - struct size size = screen->content_size; - rect.x = (size.width - size.height) / 2; - rect.y = (size.height - size.width) / 2; - rect.w = size.height; - rect.h = size.width; + rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2; + rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2; + rect.w = screen->rect.h; + rect.h = screen->rect.w; dstrect = ▭ + } else { + assert(screen->rotation == 2); + dstrect = &screen->rect; } SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect, @@ -485,7 +513,7 @@ screen_switch_fullscreen(struct screen *screen) { } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); - screen_render(screen); + screen_render(screen, true); } void @@ -523,10 +551,10 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event) { switch (event->event) { case SDL_WINDOWEVENT_EXPOSED: - screen_render(screen); + screen_render(screen, true); break; case SDL_WINDOWEVENT_SIZE_CHANGED: - screen_render(screen); + screen_render(screen, true); break; case SDL_WINDOWEVENT_MAXIMIZED: screen->maximized = true; @@ -552,6 +580,13 @@ screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; + + screen_hidpi_scale_coords(screen, &x, &y); + + x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; + y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; + + // rotate struct point result; switch (rotation) { case 0: @@ -574,3 +609,15 @@ screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { } return result; } + +void +screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) { + // take the HiDPI scaling (dw/ww and dh/wh) into account + int ww, wh, dw, dh; + SDL_GetWindowSize(screen->window, &ww, &wh); + SDL_GL_GetDrawableSize(screen->window, &dw, &dh); + + // scale for HiDPI (64 bits for intermediate multiplications) + *x = (int64_t) *x * dw / ww; + *y = (int64_t) *y * dh / wh; +} diff --git a/app/src/screen.h b/app/src/screen.h index 7703b5a2..aa6218f7 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -29,6 +29,8 @@ struct screen { // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) unsigned rotation; + // rectangle of the content (excluding black borders) + struct SDL_Rect rect; bool has_frame; bool fullscreen; bool maximized; @@ -56,6 +58,12 @@ struct screen { .height = 0, \ }, \ .rotation = 0, \ + .rect = { \ + .x = 0, \ + .y = 0, \ + .w = 0, \ + .h = 0, \ + }, \ .has_frame = false, \ .fullscreen = false, \ .maximized = false, \ @@ -89,8 +97,11 @@ bool screen_update_frame(struct screen *screen, struct video_buffer *vb); // render the texture to the renderer +// +// Set the update_content_rect flag if the window or content size may have +// changed, so that the content rectangle is recomputed void -screen_render(struct screen *screen); +screen_render(struct screen *screen, bool update_content_rect); // switch the fullscreen mode void @@ -117,4 +128,11 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); struct point screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y); +// Convert coordinates from window to drawable. +// Events are expressed in window coordinates, but content is expressed in +// drawable coordinates. They are the same if HiDPI scaling is 1, but differ +// otherwise. +void +screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y); + #endif From f5aeecbc621289fdd92f935858ed2abc4347f2b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 18 May 2020 17:56:22 +0200 Subject: [PATCH 106/214] Reset window size on initialization On macOS with renderer "metal", HiDPI scaling may be incorrect on initialization when several displays are connected. Resetting the window size fixes the problem. Refs #15 --- app/src/screen.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 967cf5d7..b92c1119 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -325,6 +325,11 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } + // Reset the window size to trigger a SIZE_CHANGED event, to workaround + // HiDPI issues with some SDL renderers when several displays having + // different HiDPI scaling are connected + SDL_SetWindowSize(screen->window, window_size.width, window_size.height); + screen_update_content_rect(screen); return true; From fae3f9eeabbb6586eadde8f720a849befd45b6a0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 23 May 2020 14:23:30 +0200 Subject: [PATCH 107/214] Remove warning when renderer is not OpenGL Trilinear filtering can currently only be enabled for OpenGL renderers. Do not print a warning if the renderer is not OpenGL, as it can confuses users, while nothing is wrong. --- app/src/screen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/screen.c b/app/src/screen.c index b92c1119..2b7ff237 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -305,7 +305,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGI("Trilinear filtering disabled"); } } else { - LOGW("Trilinear filtering disabled (not an OpenGL renderer)"); + LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); } SDL_Surface *icon = read_xpm(icon_xpm); From ac4c8b4a3f5c342ff2059fb5ca0720dab8b933f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 23 May 2020 14:32:16 +0200 Subject: [PATCH 108/214] Increase LOD bias for mipmapping Mipmapping caused too much blurring. Using a LOD bias of -1 instead of -0.5 seems a better compromise to avoid low-quality downscaling while keeping sharp edges in any case. Refs Refs --- app/src/screen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/screen.c b/app/src/screen.c index 2b7ff237..66a1163b 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -217,7 +217,7 @@ create_texture(struct screen *screen) { // Enable trilinear filtering for downscaling gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -.5f); + gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f); SDL_GL_UnbindTexture(texture); } From e1cd75792c3b6a014043b82a4089c6d34fcf78fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 May 2020 00:20:44 +0200 Subject: [PATCH 109/214] Simplify rotation watcher call Remove unnecessary private method (which was wrongly public). --- server/src/main/java/com/genymobile/scrcpy/Device.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 6aa339a1..aff04fd0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -52,7 +52,7 @@ public final class Device { screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); layerStack = displayInfo.getLayerStack(); - registerRotationWatcher(new IRotationWatcher.Stub() { + serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { synchronized (Device.this) { @@ -134,10 +134,6 @@ public final class Device { return serviceManager.getPowerManager().isScreenOn(); } - public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { - serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); - } - public synchronized void setRotationListener(RotationListener rotationListener) { this.rotationListener = rotationListener; } From 73e722784d610266d84e1bf75d1db16e9ad3c2d6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 23 May 2020 18:21:54 +0200 Subject: [PATCH 110/214] Remove useless exception declaration The interface declares it can throw a RemoteException, but the implementation never throws such exception. --- server/src/main/java/com/genymobile/scrcpy/Device.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index aff04fd0..91705359 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -54,7 +54,7 @@ public final class Device { serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @Override - public void onRotationChanged(int rotation) throws RemoteException { + public void onRotationChanged(int rotation) { synchronized (Device.this) { screenInfo = screenInfo.withDeviceRotation(rotation); From acc4ef31df8dbdfa9c1ed6170088741a45b1aead Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 May 2020 20:05:29 +0200 Subject: [PATCH 111/214] Synchronize device clipboard to computer Automatically synchronize the device clipboard to the computer any time it changes. This allows seamless copy-paste from Android to the computer. Fixes #1056 PR #1423 --- README.md | 3 ++ .../IOnPrimaryClipChangedListener.aidl | 24 +++++++++++++ .../java/com/genymobile/scrcpy/Device.java | 28 ++++++++++++++- .../java/com/genymobile/scrcpy/Server.java | 9 ++++- .../scrcpy/wrappers/ClipboardManager.java | 35 +++++++++++++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl diff --git a/README.md b/README.md index 8a1f5523..ab66827f 100644 --- a/README.md +++ b/README.md @@ -482,6 +482,9 @@ both directions: - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but breaks non-ASCII characters). +Moreover, any time the Android clipboard changes, it is automatically +synchronized to the computer clipboard. + #### Text injection preference There are two kinds of [events][textevents] generated when typing text: diff --git a/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl b/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl new file mode 100644 index 00000000..46d7f7ca --- /dev/null +++ b/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2008, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +/** + * {@hide} + */ +oneway interface IOnPrimaryClipChangedListener { + void dispatchPrimaryClipChanged(); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 91705359..6e788928 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -6,10 +6,10 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; +import android.content.IOnPrimaryClipChangedListener; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; -import android.os.RemoteException; import android.view.IRotationWatcher; import android.view.InputEvent; @@ -22,10 +22,15 @@ public final class Device { void onRotationChanged(int rotation); } + public interface ClipboardListener { + void onClipboardTextChanged(String text); + } + private final ServiceManager serviceManager = new ServiceManager(); private ScreenInfo screenInfo; private RotationListener rotationListener; + private ClipboardListener clipboardListener; /** * Logical display identifier @@ -66,6 +71,23 @@ public final class Device { } }, displayId); + if (options.getControl()) { + // If control is enabled, synchronize Android clipboard to the computer automatically + serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { + @Override + public void dispatchPrimaryClipChanged() { + synchronized (Device.this) { + if (clipboardListener != null) { + String text = getClipboardText(); + if (text != null) { + clipboardListener.onClipboardTextChanged(text); + } + } + } + } + }); + } + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } @@ -138,6 +160,10 @@ public final class Device { this.rotationListener = rotationListener; } + public synchronized void setClipboardListener(ClipboardListener clipboardListener) { + this.clipboardListener = clipboardListener; + } + public void expandNotificationPanel() { serviceManager.getStatusBarManager().expandNotificationsPanel(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 73776c3e..e2c702ba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -53,11 +53,18 @@ public final class Server { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); if (options.getControl()) { - Controller controller = new Controller(device, connection); + final Controller controller = new Controller(device, connection); // asynchronous startController(controller); startDeviceMessageSender(controller.getSender()); + + device.setClipboardListener(new Device.ClipboardListener() { + @Override + public void onClipboardTextChanged(String text) { + controller.getSender().pushClipboardText(text); + } + }); } try { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 7a3bfafb..e25b6e99 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.content.ClipData; +import android.content.IOnPrimaryClipChangedListener; import android.os.Build; import android.os.IInterface; @@ -13,6 +14,7 @@ public class ClipboardManager { private final IInterface manager; private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; + private Method addPrimaryClipChangedListener; public ClipboardManager(IInterface manager) { this.manager = manager; @@ -81,4 +83,37 @@ public class ClipboardManager { return false; } } + + private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) + throws InvocationTargetException, IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); + } else { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + } + } + + private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { + if (addPrimaryClipChangedListener == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); + } else { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + } + } + return addPrimaryClipChangedListener; + } + + public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { + try { + Method method = getAddPrimaryClipChangedListener(); + addPrimaryClipChangedListener(method, manager, listener); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return false; + } + } } From 517dbd9c85842797ec8bf21ced2ddc4040a87230 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 May 2020 14:28:58 +0200 Subject: [PATCH 112/214] Increase buffer size to fix "set clipboard" event The buffer size must be greater than any event message. Clipboard events may take up to 4096 bytes, so increase the buffer size. Fixes #1425 --- .../main/java/com/genymobile/scrcpy/ControlMessageReader.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 1c081058..2688641c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -15,7 +15,8 @@ public class ControlMessageReader { public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; public static final int INJECT_TEXT_MAX_LENGTH = 300; - private static final int RAW_BUFFER_SIZE = 1024; + + private static final int RAW_BUFFER_SIZE = 4096; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); From c7155a1954bd26e09195d8f2c2d582598d5b67dd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 May 2020 14:30:50 +0200 Subject: [PATCH 113/214] Add unit test for big "set clipboard" event Add a unit test to avoid regressions. Refs #1425 --- .../scrcpy/ControlMessageReaderTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 202126a5..d495b44c 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -229,6 +229,30 @@ public class ControlMessageReaderTest { Assert.assertEquals("testé", event.getText()); } + @Test + public void testParseBigSetClipboardEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + + byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; + Arrays.fill(rawText, (byte) 'a'); + String text = new String(rawText, 0, rawText.length); + + dos.writeShort(rawText.length); + dos.write(rawText); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(text, event.getText()); + } + @Test public void testParseSetScreenPowerMode() throws IOException { ControlMessageReader reader = new ControlMessageReader(); From 080a4ee3654a9b7e96c8ffe37474b5c21c02852a Mon Sep 17 00:00:00 2001 From: Tzah Mazuz Date: Sun, 26 Apr 2020 15:22:08 +0300 Subject: [PATCH 114/214] Add --codec-options Add a command-line parameter to pass custom options to the device encoder (as a comma-separated list of "key[:type]=value"). The list of possible codec options is available in the Android documentation: PR #1325 Refs #1226 Co-authored-by: Romain Vimont --- app/scrcpy.1 | 10 ++ app/src/cli.c | 14 +++ app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 + app/src/server.c | 1 + app/src/server.h | 1 + .../com/genymobile/scrcpy/CodecOption.java | 112 +++++++++++++++++ .../java/com/genymobile/scrcpy/Options.java | 9 ++ .../com/genymobile/scrcpy/ScreenEncoder.java | 33 ++++- .../java/com/genymobile/scrcpy/Server.java | 10 +- .../genymobile/scrcpy/CodecOptionsTest.java | 114 ++++++++++++++++++ 11 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/CodecOption.java create mode 100644 server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 02389159..6920e9a4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -25,6 +25,16 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. +.TP +.BI "\-\-codec\-options " key[:type]=value[,...] +Set a list of comma-separated key:type=value options for the device encoder. + +The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. + +The list of possible codec options is available in the Android documentation +.UR https://d.android.com/reference/android/media/MediaFormat +.UE . + .TP .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. diff --git a/app/src/cli.c b/app/src/cli.c index 0c68279f..30272828 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -30,6 +30,15 @@ scrcpy_print_usage(const char *arg0) { " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Default is %d.\n" "\n" + " --codec-options key[:type]=value[,...]\n" + " Set a list of comma-separated key:type=value options for the\n" + " device encoder.\n" + " The possible values for 'type' are 'int' (default), 'long',\n" + " 'float' and 'string'.\n" + " The list of possible codec options is available in the\n" + " Android documentation:\n" + " \n" + "\n" " --crop width:height:x:y\n" " Crop the device screen on the server.\n" " The values are expressed in the device natural orientation\n" @@ -472,12 +481,14 @@ guess_record_format(const char *filename) { #define OPT_ROTATION 1015 #define OPT_RENDER_DRIVER 1016 #define OPT_NO_MIPMAPS 1017 +#define OPT_CODEC_OPTIONS 1018 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { static const struct option long_options[] = { {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"bit-rate", required_argument, NULL, 'b'}, + {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, {"crop", required_argument, NULL, OPT_CROP}, {"display", required_argument, NULL, OPT_DISPLAY_ID}, {"fullscreen", no_argument, NULL, 'f'}, @@ -647,6 +658,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_NO_MIPMAPS: opts->mipmaps = false; break; + case OPT_CODEC_OPTIONS: + opts->codec_options = optarg; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7f8d3c75..05960630 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -303,6 +303,7 @@ scrcpy(const struct scrcpy_options *options) { .display_id = options->display_id, .show_touches = options->show_touches, .stay_awake = options->stay_awake, + .codec_options = options->codec_options, }; if (!server_start(&server, options->serial, ¶ms)) { return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 99e72ef0..af2e3bf5 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -16,6 +16,7 @@ struct scrcpy_options { const char *window_title; const char *push_target; const char *render_driver; + const char *codec_options; enum recorder_format record_format; struct port_range port_range; uint16_t max_size; @@ -48,6 +49,7 @@ struct scrcpy_options { .window_title = NULL, \ .push_target = NULL, \ .render_driver = NULL, \ + .codec_options = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \ .port_range = { \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ diff --git a/app/src/server.c b/app/src/server.c index b98cec2f..94881e51 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -270,6 +270,7 @@ execute_server(struct server *server, const struct server_params *params) { display_id_string, params->show_touches ? "true" : "false", params->stay_awake ? "true" : "false", + params->codec_options ? params->codec_options : "-", }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " diff --git a/app/src/server.h b/app/src/server.h index 13d0b451..5b51695a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,6 +44,7 @@ struct server { struct server_params { const char *crop; + const char *codec_options; struct port_range port_range; uint16_t max_size; uint32_t bit_rate; diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java new file mode 100644 index 00000000..1897bda3 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java @@ -0,0 +1,112 @@ +package com.genymobile.scrcpy; + +import java.util.ArrayList; +import java.util.List; + +public class CodecOption { + private String key; + private Object value; + + public CodecOption(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + public static List parse(String codecOptions) { + if ("-".equals(codecOptions)) { + return null; + } + + List result = new ArrayList<>(); + + boolean escape = false; + StringBuilder buf = new StringBuilder(); + + for (char c : codecOptions.toCharArray()) { + switch (c) { + case '\\': + if (escape) { + buf.append('\\'); + escape = false; + } else { + escape = true; + } + break; + case ',': + if (escape) { + buf.append(','); + escape = false; + } else { + // This comma is a separator between codec options + String codecOption = buf.toString(); + result.add(parseOption(codecOption)); + // Clear buf + buf.setLength(0); + } + break; + default: + buf.append(c); + break; + } + } + + if (buf.length() > 0) { + String codecOption = buf.toString(); + result.add(parseOption(codecOption)); + } + + return result; + } + + private static CodecOption parseOption(String option) { + int equalSignIndex = option.indexOf('='); + if (equalSignIndex == -1) { + throw new IllegalArgumentException("'=' expected"); + } + String keyAndType = option.substring(0, equalSignIndex); + if (keyAndType.length() == 0) { + throw new IllegalArgumentException("Key may not be null"); + } + + String key; + String type; + + int colonIndex = keyAndType.indexOf(':'); + if (colonIndex != -1) { + key = keyAndType.substring(0, colonIndex); + type = keyAndType.substring(colonIndex + 1); + } else { + key = keyAndType; + type = "int"; // assume int by default + } + + Object value; + String valueString = option.substring(equalSignIndex + 1); + switch (type) { + case "int": + value = Integer.parseInt(valueString); + break; + case "long": + value = Long.parseLong(valueString); + break; + case "float": + value = Float.parseFloat(valueString); + break; + case "string": + value = valueString; + break; + default: + throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type); + } + + return new CodecOption(key, value); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 152aa2f7..d2cd9a15 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -14,6 +14,7 @@ public class Options { private int displayId; private boolean showTouches; private boolean stayAwake; + private String codecOptions; public int getMaxSize() { return maxSize; @@ -102,4 +103,12 @@ public class Options { public void setStayAwake(boolean stayAwake) { this.stayAwake = stayAwake; } + + public String getCodecOptions() { + return codecOptions; + } + + public void setCodecOptions(String codecOptions) { + this.codecOptions = codecOptions; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 651affde..d722388c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -12,6 +12,7 @@ import android.view.Surface; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { @@ -25,15 +26,17 @@ public class ScreenEncoder implements Device.RotationListener { private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + private List codecOptions; private int bitRate; private int maxFps; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; + this.codecOptions = codecOptions; } @Override @@ -61,7 +64,7 @@ public class ScreenEncoder implements Device.RotationListener { } private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { - MediaFormat format = createFormat(bitRate, maxFps); + MediaFormat format = createFormat(bitRate, maxFps, codecOptions); device.setRotationListener(this); boolean alive; try { @@ -151,7 +154,24 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); } - private static MediaFormat createFormat(int bitRate, int maxFps) { + private static void setCodecOption(MediaFormat format, CodecOption codecOption) { + String key = codecOption.getKey(); + Object value = codecOption.getValue(); + + if (value instanceof Integer) { + format.setInteger(key, (Integer) value); + } else if (value instanceof Long) { + format.setLong(key, (Long) value); + } else if (value instanceof Float) { + format.setFloat(key, (Float) value); + } else if (value instanceof String) { + format.setString(key, (String) value); + } + + Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); + } + + private static MediaFormat createFormat(int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); @@ -167,6 +187,13 @@ public class ScreenEncoder implements Device.RotationListener { // format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); } + + if (codecOptions != null) { + for (CodecOption option : codecOptions) { + setCodecOption(format, option); + } + } + return format; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index e2c702ba..4ada08e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -8,6 +8,7 @@ import android.os.BatteryManager; import android.os.Build; import java.io.IOException; +import java.util.List; public final class Server { @@ -19,6 +20,7 @@ public final class Server { private static void scrcpy(Options options) throws IOException { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); + List codecOptions = CodecOption.parse(options.getCodecOptions()); boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; @@ -49,8 +51,9 @@ public final class Server { CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); boolean tunnelForward = options.isTunnelForward(); + try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions); if (options.getControl()) { final Controller controller = new Controller(device, connection); @@ -116,7 +119,7 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 12; + final int expectedParameters = 13; if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } @@ -157,6 +160,9 @@ public final class Server { boolean stayAwake = Boolean.parseBoolean(args[11]); options.setStayAwake(stayAwake); + String codecOptions = args[12]; + options.setCodecOptions(codecOptions); + return options; } diff --git a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java b/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java new file mode 100644 index 00000000..ad802258 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java @@ -0,0 +1,114 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CodecOptionsTest { + + @Test + public void testIntegerImplicit() { + List codecOptions = CodecOption.parse("some_key=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertEquals(5, option.getValue()); + } + + @Test + public void testInteger() { + List codecOptions = CodecOption.parse("some_key:int=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(5, option.getValue()); + } + + @Test + public void testLong() { + List codecOptions = CodecOption.parse("some_key:long=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Long); + Assert.assertEquals(5L, option.getValue()); + } + + @Test + public void testFloat() { + List codecOptions = CodecOption.parse("some_key:float=4.5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Float); + Assert.assertEquals(4.5f, option.getValue()); + } + + @Test + public void testString() { + List codecOptions = CodecOption.parse("some_key:string=some_value"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("some_value", option.getValue()); + } + + @Test + public void testStringEscaped() { + List codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue()); + } + + @Test + public void testList() { + List codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c"); + + Assert.assertEquals(5, codecOptions.size()); + + CodecOption option; + + option = codecOptions.get(0); + Assert.assertEquals("a", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(1, option.getValue()); + + option = codecOptions.get(1); + Assert.assertEquals("b", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(2, option.getValue()); + + option = codecOptions.get(2); + Assert.assertEquals("c", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Long); + Assert.assertEquals(3L, option.getValue()); + + option = codecOptions.get(3); + Assert.assertEquals("d", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Float); + Assert.assertEquals(4.5f, option.getValue()); + + option = codecOptions.get(4); + Assert.assertEquals("e", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("a,b=c", option.getValue()); + } +} From 56bff2f7181f1b51132f1f06682010900720cff9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 May 2020 21:11:21 +0200 Subject: [PATCH 115/214] Avoid compiler warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field lock_video_orientation may only take values between -1 and 3 (included). But the compiler may trigger a warning on the sprintf() call, because its type could represent values which could overflow the string (like "-128"): > warning: ‘%i’ directive writing between 1 and 4 bytes into a region of > size 3 [-Wformat-overflow=] Increase the buffer size to remove the warning. --- app/src/server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 94881e51..fa9230e0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -234,7 +234,7 @@ execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; char max_fps_string[6]; - char lock_video_orientation_string[3]; + char lock_video_orientation_string[5]; char display_id_string[6]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); From 3df63c579da7a35048458c6884f5f386154d2353 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 May 2020 21:08:22 +0200 Subject: [PATCH 116/214] Configure server verbosity from the client Send the requested log level from the client. This paves the way to configure it via a command-line argument. --- app/src/server.c | 5 +++ .../main/java/com/genymobile/scrcpy/Ln.java | 13 +++++++- .../java/com/genymobile/scrcpy/Options.java | 9 ++++++ .../java/com/genymobile/scrcpy/Server.java | 32 +++++++++++-------- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index fa9230e0..7cfbef0d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -259,6 +259,11 @@ execute_server(struct server *server, const struct server_params *params) { "/", // unused "com.genymobile.scrcpy.Server", SCRCPY_VERSION, +#ifndef NDEBUG + "debug", +#else + "info", +#endif max_size_string, bit_rate_string, max_fps_string, diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 26f13a56..8112bb1c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -15,12 +15,23 @@ public final class Ln { DEBUG, INFO, WARN, ERROR } - private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; + private static Level THRESHOLD; private Ln() { // not instantiable } + /** + * Initialize the log level. + *

+ * Must be called before starting any new thread. + * + * @param level the log level + */ + public static void initLogLevel(Level level) { + THRESHOLD = level; + } + public static boolean isEnabled(Level level) { return level.ordinal() >= THRESHOLD.ordinal(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d2cd9a15..06312a37 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; import android.graphics.Rect; public class Options { + private Ln.Level logLevel; private int maxSize; private int bitRate; private int maxFps; @@ -16,6 +17,14 @@ public class Options { private boolean stayAwake; private String codecOptions; + public Ln.Level getLogLevel() { + return logLevel; + } + + public void setLogLevel(Ln.Level logLevel) { + this.logLevel = logLevel; + } + public int getMaxSize() { return maxSize; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4ada08e6..54292868 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -119,48 +119,51 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 13; + final int expectedParameters = 14; if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } Options options = new Options(); - int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 + Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase()); + options.setLogLevel(level); + + int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8 options.setMaxSize(maxSize); - int bitRate = Integer.parseInt(args[2]); + int bitRate = Integer.parseInt(args[3]); options.setBitRate(bitRate); - int maxFps = Integer.parseInt(args[3]); + int maxFps = Integer.parseInt(args[4]); options.setMaxFps(maxFps); - int lockedVideoOrientation = Integer.parseInt(args[4]); + int lockedVideoOrientation = Integer.parseInt(args[5]); options.setLockedVideoOrientation(lockedVideoOrientation); // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[5]); + boolean tunnelForward = Boolean.parseBoolean(args[6]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[6]); + Rect crop = parseCrop(args[7]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[7]); + boolean sendFrameMeta = Boolean.parseBoolean(args[8]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[8]); + boolean control = Boolean.parseBoolean(args[9]); options.setControl(control); - int displayId = Integer.parseInt(args[9]); + int displayId = Integer.parseInt(args[10]); options.setDisplayId(displayId); - boolean showTouches = Boolean.parseBoolean(args[10]); + boolean showTouches = Boolean.parseBoolean(args[11]); options.setShowTouches(showTouches); - boolean stayAwake = Boolean.parseBoolean(args[11]); + boolean stayAwake = Boolean.parseBoolean(args[12]); options.setStayAwake(stayAwake); - String codecOptions = args[12]; + String codecOptions = args[13]; options.setCodecOptions(codecOptions); return options; @@ -215,6 +218,9 @@ public final class Server { }); Options options = createOptions(args); + + Ln.initLogLevel(options.getLogLevel()); + scrcpy(options); } } From a3ef461d73c4b010eacfc1f6703d38d0f2e44fad Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 May 2020 21:51:40 +0200 Subject: [PATCH 117/214] Add cli option to set the verbosity level The verbosity was set either to info (in release mode) or debug (in debug mode). Add a command-line argument to change it, so that users can enable debug logs using the release: scrcpy -Vdebug --- app/scrcpy.1 | 6 ++++++ app/src/cli.c | 44 ++++++++++++++++++++++++++++++++++++++++++-- app/src/main.c | 29 +++++++++++++++++++++++++---- app/src/scrcpy.c | 1 + app/src/scrcpy.h | 3 +++ app/src/server.c | 23 ++++++++++++++++++----- app/src/server.h | 2 ++ app/src/util/log.h | 7 +++++++ 8 files changed, 104 insertions(+), 11 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 6920e9a4..0729bc23 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -154,6 +154,12 @@ It only shows physical touches (not clicks from scrcpy). .B \-v, \-\-version Print the version of scrcpy. +.TP +.BI "\-V, \-\-verbosity " value +Set the log level ("debug", "info", "warn" or "error"). + +Default is "info" for release builds, "debug" for debug builds. + .TP .B \-w, \-\-stay-awake Keep the device on while scrcpy is running. diff --git a/app/src/cli.c b/app/src/cli.c index 30272828..b19d9a58 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -145,6 +145,14 @@ scrcpy_print_usage(const char *arg0) { "\n" " -v, --version\n" " Print the version of scrcpy.\n" + "\n" + " -V, --verbosity value\n" + " Set the log level (debug, info, warn or error).\n" +#ifndef NDEBUG + " Default is debug.\n" +#else + " Default is info.\n" +#endif "\n" " -w, --stay-awake\n" " Keep the device on while scrcpy is running.\n" @@ -433,6 +441,32 @@ parse_display_id(const char *s, uint16_t *display_id) { return true; } +static bool +parse_log_level(const char *s, enum sc_log_level *log_level) { + if (!strcmp(s, "debug")) { + *log_level = SC_LOG_LEVEL_DEBUG; + return true; + } + + if (!strcmp(s, "info")) { + *log_level = SC_LOG_LEVEL_INFO; + return true; + } + + if (!strcmp(s, "warn")) { + *log_level = SC_LOG_LEVEL_WARN; + return true; + } + + if (!strcmp(s, "error")) { + *log_level = SC_LOG_LEVEL_ERROR; + return true; + } + + LOGE("Could not parse log level: %s", s); + return false; +} + static bool parse_record_format(const char *optarg, enum recorder_format *format) { if (!strcmp(optarg, "mp4")) { @@ -513,6 +547,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"show-touches", no_argument, NULL, 't'}, {"stay-awake", no_argument, NULL, 'w'}, {"turn-screen-off", no_argument, NULL, 'S'}, + {"verbosity", required_argument, NULL, 'V'}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-x", required_argument, NULL, OPT_WINDOW_X}, @@ -529,8 +564,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { optind = 0; // reset to start from the first argument in tests int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options, - NULL)) != -1) { + while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w", + long_options, NULL)) != -1) { switch (c) { case 'b': if (!parse_bit_rate(optarg, &opts->bit_rate)) { @@ -609,6 +644,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 'v': args->version = true; break; + case 'V': + if (!parse_log_level(optarg, &opts->log_level)) { + return false; + } + break; case 'w': opts->stay_awake = true; break; diff --git a/app/src/main.c b/app/src/main.c index d683c508..85e578ae 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -29,6 +29,24 @@ print_version(void) { LIBAVUTIL_VERSION_MICRO); } +static SDL_LogPriority +convert_log_level_to_sdl(enum sc_log_level level) { + switch (level) { + case SC_LOG_LEVEL_DEBUG: + return SDL_LOG_PRIORITY_DEBUG; + case SC_LOG_LEVEL_INFO: + return SDL_LOG_PRIORITY_INFO; + case SC_LOG_LEVEL_WARN: + return SDL_LOG_PRIORITY_WARN; + case SC_LOG_LEVEL_ERROR: + return SDL_LOG_PRIORITY_ERROR; + default: + assert(!"unexpected log level"); + return SC_LOG_LEVEL_INFO; + } +} + + int main(int argc, char *argv[]) { #ifdef __WINDOWS__ @@ -38,20 +56,23 @@ main(int argc, char *argv[]) { setbuf(stderr, NULL); #endif -#ifndef NDEBUG - SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); -#endif - struct scrcpy_cli_args args = { .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, .version = false, }; +#ifndef NDEBUG + args.opts.log_level = SC_LOG_LEVEL_DEBUG; +#endif + if (!scrcpy_parse_args(&args, argc, argv)) { return 1; } + SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level); + SDL_LogSetAllPriority(sdl_log); + if (args.help) { scrcpy_print_usage(argv[0]); return 0; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 05960630..7a873391 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -293,6 +293,7 @@ bool scrcpy(const struct scrcpy_options *options) { bool record = !!options->record_filename; struct server_params params = { + .log_level = options->log_level, .crop = options->crop, .port_range = options->port_range, .max_size = options->max_size, diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index af2e3bf5..8d324378 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -8,6 +8,7 @@ #include "common.h" #include "input_manager.h" #include "recorder.h" +#include "util/log.h" struct scrcpy_options { const char *serial; @@ -17,6 +18,7 @@ struct scrcpy_options { const char *push_target; const char *render_driver; const char *codec_options; + enum sc_log_level log_level; enum recorder_format record_format; struct port_range port_range; uint16_t max_size; @@ -50,6 +52,7 @@ struct scrcpy_options { .push_target = NULL, \ .render_driver = NULL, \ .codec_options = NULL, \ + .log_level = SC_LOG_LEVEL_INFO, \ .record_format = RECORDER_FORMAT_AUTO, \ .port_range = { \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ diff --git a/app/src/server.c b/app/src/server.c index 7cfbef0d..5ec2441c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -229,6 +229,23 @@ enable_tunnel_any_port(struct server *server, struct port_range port_range) { return enable_tunnel_forward_any_port(server, port_range); } +static const char * +log_level_to_server_string(enum sc_log_level level) { + switch (level) { + case SC_LOG_LEVEL_DEBUG: + return "debug"; + case SC_LOG_LEVEL_INFO: + return "info"; + case SC_LOG_LEVEL_WARN: + return "warn"; + case SC_LOG_LEVEL_ERROR: + return "error"; + default: + assert(!"unexpected log level"); + return "(unknown)"; + } +} + static process_t execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; @@ -259,11 +276,7 @@ execute_server(struct server *server, const struct server_params *params) { "/", // unused "com.genymobile.scrcpy.Server", SCRCPY_VERSION, -#ifndef NDEBUG - "debug", -#else - "info", -#endif + log_level_to_server_string(params->log_level), max_size_string, bit_rate_string, max_fps_string, diff --git a/app/src/server.h b/app/src/server.h index 5b51695a..ff7acbdb 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -9,6 +9,7 @@ #include "config.h" #include "command.h" #include "common.h" +#include "util/log.h" #include "util/net.h" struct server { @@ -43,6 +44,7 @@ struct server { } struct server_params { + enum sc_log_level log_level; const char *crop; const char *codec_options; struct port_range port_range; diff --git a/app/src/util/log.h b/app/src/util/log.h index 5955c7fb..8c5c7dee 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -3,6 +3,13 @@ #include +enum sc_log_level { + SC_LOG_LEVEL_DEBUG, + SC_LOG_LEVEL_INFO, + SC_LOG_LEVEL_WARN, + SC_LOG_LEVEL_ERROR, +}; + #define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) From ee3882f8be22fe092bea0685be1e57e5add3935e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 May 2020 23:14:30 +0200 Subject: [PATCH 118/214] Fix typo in manpage --- app/scrcpy.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 0729bc23..3bbdbffe 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -146,7 +146,7 @@ Turn the device screen off immediately. .TP .B \-t, \-\-show\-touches -Enable "show touches" on start, restore the initial value on exit.. +Enable "show touches" on start, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). From 8f46e184262aebb3a420571de73520d60ccdfbd0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 May 2020 23:27:34 +0200 Subject: [PATCH 119/214] Add --force-adb-forward Add a command-line option to force "adb forward", without attempting "adb reverse" first. This is especially useful for using SSH tunnels without enabling remote port forwarding. --- README.md | 16 ++++++++++++++++ app/scrcpy.1 | 4 ++++ app/src/cli.c | 10 ++++++++++ app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 ++ app/src/server.c | 22 ++++++++++++++-------- app/src/server.h | 1 + 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ab66827f..3f4db9fa 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,22 @@ From another terminal: scrcpy ``` +To avoid enabling remote port forwarding, you could force a forward connection +instead (notice the `-L` instead of `-R`): + +```bash +adb kill-server # kill the local adb server on 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal: + +```bash +scrcpy --force-adb-forwrad +``` + + Like for wireless connections, it may be useful to reduce quality: ``` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 3bbdbffe..1020a2fb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -52,6 +52,10 @@ The list of possible display ids can be listed by "adb shell dumpsys display" Default is 0. +.TP +.B \-\-force\-adb\-forward +Do not attempt to use "adb reverse" to connect to the device. + .TP .B \-f, \-\-fullscreen Start in fullscreen. diff --git a/app/src/cli.c b/app/src/cli.c index b19d9a58..a0c17c1a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -54,6 +54,10 @@ scrcpy_print_usage(const char *arg0) { "\n" " Default is 0.\n" "\n" + " --force-adb-forward\n" + " Do not attempt to use \"adb reverse\" to connect to the\n" + " the device.\n" + "\n" " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" @@ -516,6 +520,7 @@ guess_record_format(const char *filename) { #define OPT_RENDER_DRIVER 1016 #define OPT_NO_MIPMAPS 1017 #define OPT_CODEC_OPTIONS 1018 +#define OPT_FORCE_ADB_FORWARD 1019 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -525,6 +530,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, {"crop", required_argument, NULL, OPT_CROP}, {"display", required_argument, NULL, OPT_DISPLAY_ID}, + {"force-adb-forward", no_argument, NULL, + OPT_FORCE_ADB_FORWARD}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"lock-video-orientation", required_argument, NULL, @@ -701,6 +708,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_CODEC_OPTIONS: opts->codec_options = optarg; break; + case OPT_FORCE_ADB_FORWARD: + opts->force_adb_forward = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7a873391..67ebf8c0 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -305,6 +305,7 @@ scrcpy(const struct scrcpy_options *options) { .show_touches = options->show_touches, .stay_awake = options->stay_awake, .codec_options = options->codec_options, + .force_adb_forward = options->force_adb_forward, }; if (!server_start(&server, options->serial, ¶ms)) { return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8d324378..70d99433 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -42,6 +42,7 @@ struct scrcpy_options { bool window_borderless; bool mipmaps; bool stay_awake; + bool force_adb_forward; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -79,6 +80,7 @@ struct scrcpy_options { .window_borderless = false, \ .mipmaps = true, \ .stay_awake = false, \ + .force_adb_forward = false, \ } bool diff --git a/app/src/server.c b/app/src/server.c index 5ec2441c..fb498d63 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -217,15 +217,20 @@ enable_tunnel_forward_any_port(struct server *server, } static bool -enable_tunnel_any_port(struct server *server, struct port_range port_range) { - if (enable_tunnel_reverse_any_port(server, port_range)) { - return true; +enable_tunnel_any_port(struct server *server, struct port_range port_range, + bool force_adb_forward) { + if (!force_adb_forward) { + // Attempt to use "adb reverse" + if (enable_tunnel_reverse_any_port(server, port_range)) { + return true; + } + + // if "adb reverse" does not work (e.g. over "adb connect"), it + // fallbacks to "adb forward", so the app socket is the client + + LOGW("'adb reverse' failed, fallback to 'adb forward'"); } - // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to - // "adb forward", so the app socket is the client - - LOGW("'adb reverse' failed, fallback to 'adb forward'"); return enable_tunnel_forward_any_port(server, port_range); } @@ -384,7 +389,8 @@ server_start(struct server *server, const char *serial, goto error1; } - if (!enable_tunnel_any_port(server, params->port_range)) { + if (!enable_tunnel_any_port(server, params->port_range, + params->force_adb_forward)) { goto error1; } diff --git a/app/src/server.h b/app/src/server.h index ff7acbdb..2215d817 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -56,6 +56,7 @@ struct server_params { uint16_t display_id; bool show_touches; bool stay_awake; + bool force_adb_forward; }; // init default values From 5c2cf88a1dd80cbf9752bb500c3d1bd426316926 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 May 2020 03:22:07 +0200 Subject: [PATCH 120/214] Rename THRESHOLD to threshold Since the field is not final anymore, lint expects the name not to be capitalized. --- server/src/main/java/com/genymobile/scrcpy/Ln.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 8112bb1c..4013315f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -15,7 +15,7 @@ public final class Ln { DEBUG, INFO, WARN, ERROR } - private static Level THRESHOLD; + private static Level threshold; private Ln() { // not instantiable @@ -29,11 +29,11 @@ public final class Ln { * @param level the log level */ public static void initLogLevel(Level level) { - THRESHOLD = level; + threshold = level; } public static boolean isEnabled(Level level) { - return level.ordinal() >= THRESHOLD.ordinal(); + return level.ordinal() >= threshold.ordinal(); } public static void d(String message) { From 81573d81a078021bf17a7e5b76ecd9633c7d0fad Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 May 2020 03:28:15 +0200 Subject: [PATCH 121/214] Pass a Locale to toUpperCase() Make lint happy. --- server/src/main/java/com/genymobile/scrcpy/Server.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 54292868..44b3afd4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -9,6 +9,7 @@ import android.os.Build; import java.io.IOException; import java.util.List; +import java.util.Locale; public final class Server { @@ -126,7 +127,7 @@ public final class Server { Options options = new Options(); - Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase()); + Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH)); options.setLogLevel(level); int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8 From c7a33fac36576817f092188f54b140e71edd8412 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 May 2020 02:23:32 +0200 Subject: [PATCH 122/214] Log actions on the caller side Some actions are exposed by the Device class, but logging success should be done by the caller. --- .../java/com/genymobile/scrcpy/Controller.java | 11 +++++++++-- .../main/java/com/genymobile/scrcpy/Device.java | 16 +++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index eb5a0805..4442188c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -110,11 +110,18 @@ public class Controller { sender.pushClipboardText(clipboardText); break; case ControlMessage.TYPE_SET_CLIPBOARD: - device.setClipboardText(msg.getText()); + boolean setClipboardOk = device.setClipboardText(msg.getText()); + if (setClipboardOk) { + Ln.i("Device clipboard set"); + } break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { - device.setScreenPowerMode(msg.getAction()); + int mode = msg.getAction(); + boolean setPowerModeOk = device.setScreenPowerMode(mode); + if (setPowerModeOk) { + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } } break; case ControlMessage.TYPE_ROTATE_DEVICE: diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 6e788928..1a851b15 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -180,26 +180,20 @@ public final class Device { return s.toString(); } - public void setClipboardText(String text) { - boolean ok = serviceManager.getClipboardManager().setText(text); - if (ok) { - Ln.i("Device clipboard set"); - } + public boolean setClipboardText(String text) { + return serviceManager.getClipboardManager().setText(text); } /** * @param mode one of the {@code SCREEN_POWER_MODE_*} constants */ - public void setScreenPowerMode(int mode) { + public boolean setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); - return; - } - boolean ok = SurfaceControl.setDisplayPowerMode(d, mode); - if (ok) { - Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + return false; } + return SurfaceControl.setDisplayPowerMode(d, mode); } /** From ffc57512b3241d28424cd883985d66ac92698cf3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 May 2020 02:58:06 +0200 Subject: [PATCH 123/214] Avoid clipboard synchronization loop The Android device listens for clipboard changes to synchronize with the computer clipboard. However, if the change comes from scrcpy (for example via Ctrl+Shift+v), do not notify the change. --- .../src/main/java/com/genymobile/scrcpy/Device.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 1a851b15..ec28bd7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -13,6 +13,8 @@ import android.os.IBinder; import android.view.IRotationWatcher; import android.view.InputEvent; +import java.util.concurrent.atomic.AtomicBoolean; + public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; @@ -31,6 +33,7 @@ public final class Device { private ScreenInfo screenInfo; private RotationListener rotationListener; private ClipboardListener clipboardListener; + private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); /** * Logical display identifier @@ -76,6 +79,10 @@ public final class Device { serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { @Override public void dispatchPrimaryClipChanged() { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } synchronized (Device.this) { if (clipboardListener != null) { String text = getClipboardText(); @@ -181,7 +188,10 @@ public final class Device { } public boolean setClipboardText(String text) { - return serviceManager.getClipboardManager().setText(text); + isSettingClipboard.set(true); + boolean ok = serviceManager.getClipboardManager().setText(text); + isSettingClipboard.set(false); + return ok; } /** From 4bbabfb4ef5f1105215dc2bb8a282ffce221bbcc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 May 2020 02:32:37 +0200 Subject: [PATCH 124/214] Move injection methods to Device Only the main injection method was exposed on Device, the convenience methods were implemented in Controller. For consistency, move them all to the Device class. --- .../com/genymobile/scrcpy/Controller.java | 30 ++++--------------- .../java/com/genymobile/scrcpy/Device.java | 21 ++++++++++++- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 4442188c..ab7b6c40 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,10 +1,7 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.InputManager; - import android.os.SystemClock; import android.view.InputDevice; -import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -50,7 +47,7 @@ public class Controller { public void control() throws IOException { // on start, power on the device if (!device.isScreenOn()) { - injectKeycode(KeyEvent.KEYCODE_POWER); + device.injectKeycode(KeyEvent.KEYCODE_POWER); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -133,7 +130,7 @@ public class Controller { } private boolean injectKeycode(int action, int keycode, int metaState) { - return injectKeyEvent(action, keycode, 0, metaState); + return device.injectKeyEvent(action, keycode, 0, metaState); } private boolean injectChar(char c) { @@ -144,7 +141,7 @@ public class Controller { return false; } for (KeyEvent event : events) { - if (!injectEvent(event)) { + if (!device.injectEvent(event)) { return false; } } @@ -200,7 +197,7 @@ public class Controller { MotionEvent event = MotionEvent .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); - return injectEvent(event); + return device.injectEvent(event); } private boolean injectScroll(Position position, int hScroll, int vScroll) { @@ -223,26 +220,11 @@ public class Controller { MotionEvent event = MotionEvent .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); - return injectEvent(event); - } - - private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { - long now = SystemClock.uptimeMillis(); - KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, - InputDevice.SOURCE_KEYBOARD); - return injectEvent(event); - } - - private boolean injectKeycode(int keyCode) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); - } - - private boolean injectEvent(InputEvent event) { - return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + return device.injectEvent(event); } private boolean pressBackOrTurnScreenOn() { int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; - return injectKeycode(keycode); + return device.injectKeycode(keycode); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index ec28bd7c..349486c3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -10,8 +10,12 @@ import android.content.IOnPrimaryClipChangedListener; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; +import android.os.SystemClock; import android.view.IRotationWatcher; +import android.view.InputDevice; import android.view.InputEvent; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; import java.util.concurrent.atomic.AtomicBoolean; @@ -147,7 +151,7 @@ public final class Device { return supportsInputEvents; } - public boolean injectInputEvent(InputEvent inputEvent, int mode) { + public boolean injectEvent(InputEvent inputEvent, int mode) { if (!supportsInputEvents()) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); } @@ -159,6 +163,21 @@ public final class Device { return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); } + public boolean injectEvent(InputEvent event) { + return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + } + + public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + return injectEvent(event); + } + + public boolean injectKeycode(int keyCode) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + } + public boolean isScreenOn() { return serviceManager.getPowerManager().isScreenOn(); } From 274b591d1867eacd9cffd6a1b38c906253023f8e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 May 2020 18:41:05 +0200 Subject: [PATCH 125/214] Fix union typo The "set clipboard" event used the wrong union type to store its text. In practice, it worked because both are at the same offset. --- app/src/control_msg.c | 2 +- app/tests/test_control_msg_serialize.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 252a3425..72504138 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -67,7 +67,7 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { (uint32_t) msg->inject_scroll_event.vscroll); return 21; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { - size_t len = write_string(msg->inject_text.text, + size_t len = write_string(msg->set_clipboard.text, CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 4dc79018..da243d91 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -200,7 +200,7 @@ static void test_serialize_get_clipboard(void) { static void test_serialize_set_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, - .inject_text = { + .set_clipboard = { .text = "hello, world!", }, }; From fc1dec027093ee8a2746a212db0c72a9d96d39a4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 May 2020 20:58:24 +0200 Subject: [PATCH 126/214] Paste on "set clipboard" if possible Ctrl+Shift+v synchronizes the computer clipboard to the Android device clipboard. This feature had been added to provide a way to copy UTF-8 text from the computer to the device. To make such a paste more straightforward, if the device runs at least Android 7, also send a PASTE keycode to paste immediately. Fixes #786 --- README.md | 49 ++++++++++--------- app/scrcpy.1 | 2 +- app/src/cli.c | 3 +- app/src/control_msg.c | 5 +- app/src/control_msg.h | 5 +- app/src/input_manager.c | 7 +-- app/tests/test_control_msg_serialize.c | 4 +- .../com/genymobile/scrcpy/ControlMessage.java | 12 ++++- .../scrcpy/ControlMessageReader.java | 9 +++- .../com/genymobile/scrcpy/Controller.java | 21 ++++++-- .../scrcpy/ControlMessageReaderTest.java | 8 +++ 11 files changed, 84 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3f4db9fa..7cc7e1fd 100644 --- a/README.md +++ b/README.md @@ -494,7 +494,8 @@ It is possible to synchronize clipboards between the computer and the device, in both directions: - `Ctrl`+`c` copies the device clipboard to the computer clipboard; - - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard; + - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard (and + pastes if the device runs Android >= 7); - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but breaks non-ASCII characters). @@ -559,29 +560,29 @@ Also see [issue #14]. ## Shortcuts - | Action | Shortcut | Shortcut (macOS) - | -------------------------------------- |:----------------------------- |:----------------------------- - | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` - | Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ - | Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ - | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` - | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ - | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ - | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ - | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` - | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` - | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ - | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ - | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` - | Power on | _Right-click²_ | _Right-click²_ - | Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` - | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` - | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` - | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` - | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` - | Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v` - | Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` - | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` + | Action | Shortcut | Shortcut (macOS) + | ------------------------------------------- |:----------------------------- |:----------------------------- + | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` + | Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ + | Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ + | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` + | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ + | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ + | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ + | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` + | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` + | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ + | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ + | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` + | Power on | _Right-click²_ | _Right-click²_ + | Turn device screen off (keep mirroring) | `Ctrl`+`o` | `Cmd`+`o` + | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` + | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` + | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` + | Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v` + | Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1020a2fb..ed4594ed 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -280,7 +280,7 @@ Paste computer clipboard to device .TP .B Ctrl+Shift+v -Copy computer clipboard to device +Copy computer clipboard to device (and paste if the device runs Android >= 7) .TP .B Ctrl+i diff --git a/app/src/cli.c b/app/src/cli.c index a0c17c1a..836edc30 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -247,7 +247,8 @@ scrcpy_print_usage(const char *arg0) { " Paste computer clipboard to device\n" "\n" " " CTRL_OR_CMD "+Shift+v\n" - " Copy computer clipboard to device\n" + " Copy computer clipboard to device (and paste if the device\n" + " runs Android >= 7)\n" "\n" " " CTRL_OR_CMD "+i\n" " Enable/disable FPS counter (print frames/second in logs)\n" diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 72504138..c5778c02 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -67,10 +67,11 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { (uint32_t) msg->inject_scroll_event.vscroll); return 21; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { + buf[1] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[1]); - return 1 + len; + &buf[2]); + return 2 + len; } case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index e132fc6b..0e85c97e 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -11,9 +11,9 @@ #include "common.h" #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4092 #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ - (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) + (4 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) #define POINTER_ID_MOUSE UINT64_C(-1); @@ -62,6 +62,7 @@ struct control_msg { } inject_scroll_event; struct { char *text; // owned, to be freed by SDL_free() + bool paste; } set_clipboard; struct { enum screen_power_mode mode; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e8c8d68e..3cb9e3ac 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -112,7 +112,7 @@ request_device_clipboard(struct controller *controller) { } static void -set_device_clipboard(struct controller *controller) { +set_device_clipboard(struct controller *controller, bool paste) { char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); @@ -127,6 +127,7 @@ set_device_clipboard(struct controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; msg.set_clipboard.text = text; + msg.set_clipboard.paste = paste; if (!controller_push_msg(controller, &msg)) { SDL_free(text); @@ -354,8 +355,8 @@ input_manager_process_key(struct input_manager *im, case SDLK_v: if (control && cmd && !repeat && down) { if (shift) { - // store the text in the device clipboard - set_device_clipboard(controller); + // store the text in the device clipboard and paste + set_device_clipboard(controller, true); } else { // inject the text as input events clipboard_paste(controller); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index da243d91..c6ff7b2e 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -201,16 +201,18 @@ static void test_serialize_set_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { + .paste = true, .text = "hello, world!", }, }; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 16); + assert(size == 17); const unsigned char expected[] = { CONTROL_MSG_TYPE_SET_CLIPBOARD, + 1, // paste 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 195b04bf..7d0ab7a6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -17,6 +17,8 @@ public final class ControlMessage { public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_ROTATE_DEVICE = 10; + public static final int FLAGS_PASTE = 1; + private int type; private String text; private int metaState; // KeyEvent.META_* @@ -28,6 +30,7 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; + private int flags; private ControlMessage() { } @@ -68,10 +71,13 @@ public final class ControlMessage { return msg; } - public static ControlMessage createSetClipboard(String text) { + public static ControlMessage createSetClipboard(String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; msg.text = text; + if (paste) { + msg.flags = FLAGS_PASTE; + } return msg; } @@ -134,4 +140,8 @@ public final class ControlMessage { public int getVScroll() { return vScroll; } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 2688641c..fbf49a61 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -12,8 +12,9 @@ public class ControlMessageReader { static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length) public static final int INJECT_TEXT_MAX_LENGTH = 300; private static final int RAW_BUFFER_SIZE = 4096; @@ -148,11 +149,15 @@ public class ControlMessageReader { } private ControlMessage parseSetClipboard() { + if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { + return null; + } + boolean parse = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text); + return ControlMessage.createSetClipboard(text, parse); } private ControlMessage parseSetScreenPowerMode() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index ab7b6c40..4999f7eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; import android.view.KeyCharacterMap; @@ -107,10 +108,8 @@ public class Controller { sender.pushClipboardText(clipboardText); break; case ControlMessage.TYPE_SET_CLIPBOARD: - boolean setClipboardOk = device.setClipboardText(msg.getText()); - if (setClipboardOk) { - Ln.i("Device clipboard set"); - } + boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + setClipboard(msg.getText(), paste); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { @@ -227,4 +226,18 @@ public class Controller { int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; return device.injectKeycode(keycode); } + + private boolean setClipboard(String text, boolean paste) { + boolean ok = device.setClipboardText(text); + if (ok) { + Ln.i("Device clipboard set"); + } + + // On Android >= 7, also press the PASTE key if requested + if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + device.injectKeycode(KeyEvent.KEYCODE_PASTE); + } + + return ok; + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index d495b44c..f5fa4d09 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -216,6 +216,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + dos.writeByte(1); // paste byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeShort(text.length); dos.write(text); @@ -227,6 +228,9 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals("testé", event.getText()); + + boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + Assert.assertTrue(parse); } @Test @@ -238,6 +242,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; + dos.writeByte(1); // paste Arrays.fill(rawText, (byte) 'a'); String text = new String(rawText, 0, rawText.length); @@ -251,6 +256,9 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(text, event.getText()); + + boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + Assert.assertTrue(parse); } @Test From 8f619f337baec26791514632c64c0385585c2f06 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 May 2020 19:21:05 +0200 Subject: [PATCH 127/214] Upgrade platform-tools (30.0.0) for Windows Include the latest version of adb in Windows releases. --- prebuilt-deps/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 088cee92..c6cbcf7b 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.12 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ - 2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.0-windows.zip \ + 854305f9a702f5ea2c3de73edde402bd26afa0ee944c9b0c4380420f5a862e0d \ platform-tools From d499ee53c9fa63fb02db00dc09fdd4425f506ef6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 May 2020 12:05:29 +0200 Subject: [PATCH 128/214] Initialize a default log level Clean up has been broken by 3df63c579da7a35048458c6884f5f386154d2353. The verbosity was correctly initialized for the Server process, but not for the CleanUp process. To avoid the problem, initialize a default log level. --- server/src/main/java/com/genymobile/scrcpy/Ln.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 4013315f..c218fa0f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -15,7 +15,7 @@ public final class Ln { DEBUG, INFO, WARN, ERROR } - private static Level threshold; + private static Level threshold = Level.INFO; private Ln() { // not instantiable From 2ca8318b9df10f24de138ab59ab41d589db26fb3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 May 2020 12:38:59 +0200 Subject: [PATCH 129/214] Improve manpage formatting Add a new line to avoid unwanted text justification --- app/scrcpy.1 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index ed4594ed..e5568849 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -129,6 +129,7 @@ Force recording format (either mp4 or mkv). Request SDL to use the given render driver (this is just a hint). Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". + .UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER .UE From 93a5c5149d6b4d04376f147e6da2f5ca3ced8aea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 May 2020 18:12:35 +0200 Subject: [PATCH 130/214] Push clipboard text only if not null In practice, it does not change anything (it just avoids a spurious wake-up), but semantically, it makes no sense to call pushClipboardText() with a null value. --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 4999f7eb..960c6a6e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -105,7 +105,9 @@ public class Controller { break; case ControlMessage.TYPE_GET_CLIPBOARD: String clipboardText = device.getClipboardText(); - sender.pushClipboardText(clipboardText); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + } break; case ControlMessage.TYPE_SET_CLIPBOARD: boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; From dcde578a50ffb9d2594c4764a067e9dd328bf55c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 May 2020 18:18:39 +0200 Subject: [PATCH 131/214] Reactivate "turn device screen on" feature This reverts commit 8c8649cfcd710859ce18eab557ed2af8cedb9a42. I cannot reproduce the issue with Ctrl+Shift+o on any device, so in practice it works, it's too bad to remove the feature for a random bug on some Android versions on some devices. --- README.md | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 3 +++ app/src/input_manager.c | 7 +++++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7cc7e1fd..fe4c8584 100644 --- a/README.md +++ b/README.md @@ -576,6 +576,7 @@ Also see [issue #14]. | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` | Power on | _Right-click²_ | _Right-click²_ | Turn device screen off (keep mirroring) | `Ctrl`+`o` | `Cmd`+`o` + | Turn device screen on | `Ctrl`+`Shift`+`o` | `Cmd`+`Shift`+`o` | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e5568849..de4a70e4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -259,6 +259,10 @@ Turn screen on .B Ctrl+o Turn device screen off (keep mirroring) +.TP +.B Ctrl+Shift+o +Turn device screen on + .TP .B Ctrl+r Rotate device screen diff --git a/app/src/cli.c b/app/src/cli.c index 836edc30..0be1bd75 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -231,6 +231,9 @@ scrcpy_print_usage(const char *arg0) { " " CTRL_OR_CMD "+o\n" " Turn device screen off (keep mirroring)\n" "\n" + " " CTRL_OR_CMD "+Shift+o\n" + " Turn device screen on\n" + "\n" " " CTRL_OR_CMD "+r\n" " Rotate device screen\n" "\n" diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3cb9e3ac..e8ba9f79 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -321,8 +321,11 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_o: - if (control && cmd && !shift && down) { - set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF); + if (control && cmd && down) { + enum screen_power_mode mode = shift + ? SCREEN_POWER_MODE_NORMAL + : SCREEN_POWER_MODE_OFF; + set_screen_power_mode(controller, mode); } return; case SDLK_DOWN: From 44fa4a090eb7c64678a2034535da3ee09884b657 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 May 2020 18:19:33 +0200 Subject: [PATCH 132/214] Bump version to 1.14 --- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 49093453..46b9a092 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.13', + version: '1.14', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 3fa16519..c8ff85d6 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 15 - versionName "1.13" + versionCode 16 + versionName "1.14" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 06fc0d75..f6889701 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=1.13 +SCRCPY_VERSION_NAME=1.14 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From ef91ab2841db16d8ac192813cc8cdca7552f8a12 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 May 2020 19:30:56 +0200 Subject: [PATCH 133/214] Update links to v1.14 in README and BUILD --- BUILD.md | 6 +++--- README.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BUILD.md b/BUILD.md index e4f175ef..3696e6b0 100644 --- a/BUILD.md +++ b/BUILD.md @@ -249,10 +249,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.13`][direct-scrcpy-server] - _(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_ + - [`scrcpy-server-v1.14`][direct-scrcpy-server] + _(SHA-256: 1d1b18a2b80e956771fd63b99b414d2d028713a8f12ddfa5a369709ad4295620)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-server-v1.14 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index fe4c8584..0162de0d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.13) +# scrcpy (v1.14) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -69,10 +69,10 @@ hard). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.13.zip`][direct-win64] - _(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_ + - [`scrcpy-win64-v1.14.zip`][direct-win64] + _(SHA-256: 2be9139e46e29cf2f5f695848bb2b75a543b8f38be1133257dc5068252abc25f)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-win64-v1.14.zip It is also available in [Chocolatey]: From 8b73c90427e5f31d031357c2b319512b57ef78f5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 May 2020 19:32:02 +0200 Subject: [PATCH 134/214] Mention how to turn the screen on in README Now that Ctrl+Shift+o has been reactivated, mention it in the "turn screen off" section. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0162de0d..6f698292 100644 --- a/README.md +++ b/README.md @@ -439,7 +439,7 @@ scrcpy -S Or by pressing `Ctrl`+`o` at any time. -To turn it back on, press `POWER` (or `Ctrl`+`p`). +To turn it back on, press `Ctrl`+`Shift`+`o` (or `POWER`, `Ctrl`+`p`). It can be useful to also prevent the device to sleep: From 0e4a6f462bcc628af00896eea38aa883d68acc88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 May 2020 23:07:28 +0200 Subject: [PATCH 135/214] Mention stay awake limitation The "stay awake" feature only works when the device is plugged in. Refs #1445 --- README.md | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f698292..fe7f0fac 100644 --- a/README.md +++ b/README.md @@ -417,7 +417,7 @@ The secondary display may only be controlled if the device runs at least Android #### Stay awake -To prevent the device to sleep after some delay: +To prevent the device to sleep after some delay when the device is plugged in: ```bash scrcpy --stay-awake diff --git a/app/scrcpy.1 b/app/scrcpy.1 index de4a70e4..776d78ae 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -167,7 +167,7 @@ Default is "info" for release builds, "debug" for debug builds. .TP .B \-w, \-\-stay-awake -Keep the device on while scrcpy is running. +Keep the device on while scrcpy is running, when the device is plugged in. .TP .B \-\-window\-borderless diff --git a/app/src/cli.c b/app/src/cli.c index 0be1bd75..be0b7c23 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -159,7 +159,8 @@ scrcpy_print_usage(const char *arg0) { #endif "\n" " -w, --stay-awake\n" - " Keep the device on while scrcpy is running.\n" + " Keep the device on while scrcpy is running, when the device\n" + " is plugged in.\n" "\n" " --window-borderless\n" " Disable window decorations (display borderless window).\n" From e4efd757666c0a29876783dd916043ebebbec493 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 29 May 2020 22:02:41 +0200 Subject: [PATCH 136/214] Avoid repetition for some shortcuts Keeping the key pressed generate "repeat" events. It does not make sense to repeat the event for rotation or turn screen off. --- app/src/input_manager.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e8ba9f79..54e619bf 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -321,7 +321,7 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_o: - if (control && cmd && down) { + if (control && cmd && !repeat && down) { enum screen_power_mode mode = shift ? SCREEN_POWER_MODE_NORMAL : SCREEN_POWER_MODE_OFF; @@ -341,12 +341,12 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_LEFT: - if (cmd && !shift && down) { + if (cmd && !shift && !repeat && down) { rotate_client_left(im->screen); } return; case SDLK_RIGHT: - if (cmd && !shift && down) { + if (cmd && !shift && !repeat && down) { rotate_client_right(im->screen); } return; From 8ff07e0c88c7d267cbf00fd9e63f8c9f9dc3c952 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 2 Jun 2020 18:17:25 +0200 Subject: [PATCH 137/214] Git-ignore release directories --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 222769b3..7bc30289 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ build/ /dist/ +/build-*/ +/build_*/ +/release-*/ .idea/ .gradle/ /x/ From c4323df97607d345f35d832cd373b4882b73a9ac Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 2 Jun 2020 18:18:58 +0200 Subject: [PATCH 138/214] Fix incorrect log return value The function must return a SDL_LogPriority, but returned an enum sc_log_level. (It was harmless because this specific return should never happen, as asserted.) --- app/src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main.c b/app/src/main.c index 85e578ae..29c9b423 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -42,7 +42,7 @@ convert_log_level_to_sdl(enum sc_log_level level) { return SDL_LOG_PRIORITY_ERROR; default: assert(!"unexpected log level"); - return SC_LOG_LEVEL_INFO; + return SDL_LOG_PRIORITY_INFO; } } From 6e1069a8228217d39b87f6a490a4c2c75258c16a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 2 Jun 2020 18:21:01 +0200 Subject: [PATCH 139/214] Configure log level for application only Do not expose internal SDL logs to users. Fixes #1441 --- app/src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main.c b/app/src/main.c index 29c9b423..7d7ddb82 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -71,7 +71,7 @@ main(int argc, char *argv[]) { } SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level); - SDL_LogSetAllPriority(sdl_log); + SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); if (args.help) { scrcpy_print_usage(argv[0]); From 1b73eff3c95a3e1875f0db51a60179f3d282ea57 Mon Sep 17 00:00:00 2001 From: Louis Leseur Date: Sun, 7 Jun 2020 00:02:21 +0200 Subject: [PATCH 140/214] Add missing file in build_without_gradle.sh Fixes #1481 PR #1482 Signed-off-by: Louis Leseur Signed-off-by: Romain Vimont --- server/build_without_gradle.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index f6889701..e3be2faf 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -42,6 +42,8 @@ echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" "$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ android/view/IRotationWatcher.aidl +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ + android/content/IOnPrimaryClipChangedListener.aidl echo "Compiling java sources..." cd ../java @@ -55,6 +57,7 @@ cd "$CLASSES_DIR" "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ --output "$BUILD_DIR/classes.dex" \ android/view/*.class \ + android/content/*.class \ com/genymobile/scrcpy/*.class \ com/genymobile/scrcpy/wrappers/*.class From 3c0fc8f54f42bf6e7eca35b352a7d343749b65c4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 9 Jun 2020 22:08:34 +0200 Subject: [PATCH 141/214] Mention sndcpy --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fe7f0fac..df3e1172 100644 --- a/README.md +++ b/README.md @@ -550,11 +550,11 @@ scrcpy --push-target /sdcard/foo/bar/ ### Audio forwarding -Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). +Audio is not forwarded by _scrcpy_. Use [sndcpy]. Also see [issue #14]. -[USBaudio]: https://github.com/rom1v/usbaudio +[sndcpy]: https://github.com/rom1v/sndcpy [issue #14]: https://github.com/Genymobile/scrcpy/issues/14 From 8f314c74b073a7f844de16539657a264f05662d6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 21:09:42 +0200 Subject: [PATCH 142/214] Reorganize message size constants Make the max clipboard text length depend on the max message size. --- app/src/control_msg.h | 8 +++++--- app/src/device_msg.h | 5 +++-- .../java/com/genymobile/scrcpy/ControlMessageReader.java | 8 ++++---- .../java/com/genymobile/scrcpy/DeviceMessageWriter.java | 6 +++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 0e85c97e..1a5449a0 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,10 +10,12 @@ #include "android/keycodes.h" #include "common.h" +#define CONTROL_MSG_SERIALIZED_MAX_SIZE 4096 + #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4092 -#define CONTROL_MSG_SERIALIZED_MAX_SIZE \ - (4 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) +// type: 1 byte; paste flag: 1 byte; length: 2 bytes +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH \ + (CONTROL_MSG_SERIALIZED_MAX_SIZE - 4) #define POINTER_ID_MOUSE UINT64_C(-1); diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 04723597..f19630bf 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -7,8 +7,9 @@ #include "config.h" -#define DEVICE_MSG_TEXT_MAX_LENGTH 4093 -#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) +#define DEVICE_MSG_SERIALIZED_MAX_SIZE 4096 +// type: 1 byte; length: 2 bytes +#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_SERIALIZED_MAX_SIZE - 3) enum device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index fbf49a61..f80ffebb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -14,12 +14,12 @@ public class ControlMessageReader { static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length) + private static final int MESSAGE_MAX_SIZE = 4096; + + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 4; // type: 1 byte; paste flag: 1 byte; length: 2 bytes public static final int INJECT_TEXT_MAX_LENGTH = 300; - private static final int RAW_BUFFER_SIZE = 4096; - - private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; + private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index 6c7f3634..ea54d533 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -7,10 +7,10 @@ import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; - private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; + private static final int MESSAGE_MAX_SIZE = 4096; + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 3; // type: 1 byte; length: 2 bytes - private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; + private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { From a00a8763d6ebc240ab06380c04a762d10dfc7939 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 18:33:42 +0200 Subject: [PATCH 143/214] Avoid additional copy on Java text parsing Directly pass the buffer range to the String constructor. --- .../java/com/genymobile/scrcpy/ControlMessageReader.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index f80ffebb..b8fba824 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -21,7 +21,6 @@ public class ControlMessageReader { private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; public ControlMessageReader() { // invariant: the buffer is always in "get" mode @@ -111,8 +110,10 @@ public class ControlMessageReader { if (buffer.remaining() < len) { return null; } - buffer.get(textBuffer, 0, len); - return new String(textBuffer, 0, len, StandardCharsets.UTF_8); + int position = buffer.position(); + // Move the buffer position to consume the text + buffer.position(position + len); + return new String(rawBuffer, position, len, StandardCharsets.UTF_8); } private ControlMessage parseInjectText() { From 08c0c64af64cf124c46f0922447f81fc804a74b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 21:43:07 +0200 Subject: [PATCH 144/214] Rename test names from "event" to "msg" The meson test names had not been changed when "event" had been renamed to "message". Ref: 28980bbc90ab93cf61bdc4b36d2448b8cebbb7df --- app/meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/meson.build b/app/meson.build index 5d2b4caa..505f0819 100644 --- a/app/meson.build +++ b/app/meson.build @@ -164,12 +164,12 @@ if get_option('buildtype') == 'debug' 'src/cli.c', 'src/util/str_util.c', ]], - ['test_control_event_serialize', [ + ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', 'src/util/str_util.c', ]], - ['test_device_event_deserialize', [ + ['test_device_msg_deserialize', [ 'tests/test_device_msg_deserialize.c', 'src/device_msg.c', ]], From d202d7b2059eb440c044477993a6e0d6aa4d5086 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 21:22:47 +0200 Subject: [PATCH 145/214] Add unit test for big clipboard device message Test clipboard synchronization from the device to the computer. --- app/tests/test_device_msg_deserialize.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index e163ad72..2e29d087 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -4,6 +4,7 @@ #include "device_msg.h" #include + static void test_deserialize_clipboard(void) { const unsigned char input[] = { DEVICE_MSG_TYPE_CLIPBOARD, @@ -22,7 +23,28 @@ static void test_deserialize_clipboard(void) { device_msg_destroy(&msg); } +static void test_deserialize_clipboard_big(void) { + unsigned char input[DEVICE_MSG_SERIALIZED_MAX_SIZE]; + input[0] = DEVICE_MSG_TYPE_CLIPBOARD; + input[1] = DEVICE_MSG_TEXT_MAX_LENGTH >> 8; // MSB + input[2] = DEVICE_MSG_TEXT_MAX_LENGTH & 0xff; // LSB + + memset(input + 3, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); + + struct device_msg msg; + ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + assert(r == DEVICE_MSG_SERIALIZED_MAX_SIZE); + + assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); + assert(msg.clipboard.text); + assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); + assert(msg.clipboard.text[0] == 'a'); + + device_msg_destroy(&msg); +} + int main(void) { test_deserialize_clipboard(); + test_deserialize_clipboard_big(); return 0; } From d91c5dcfd54fa98a06be938da83fdad8e85e3724 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 21:26:38 +0200 Subject: [PATCH 146/214] Rename MSG_SERIALIZED_MAX_SIZE to MSG_MAX_SIZE For simplicity and consistency with the server part. --- app/src/control_msg.h | 7 +++---- app/src/controller.c | 2 +- app/src/device_msg.h | 4 ++-- app/src/receiver.c | 6 +++--- app/tests/test_control_msg_serialize.c | 24 ++++++++++++------------ app/tests/test_device_msg_deserialize.c | 4 ++-- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 1a5449a0..f5d633a8 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,12 +10,11 @@ #include "android/keycodes.h" #include "common.h" -#define CONTROL_MSG_SERIALIZED_MAX_SIZE 4096 +#define CONTROL_MSG_MAX_SIZE 4096 #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 // type: 1 byte; paste flag: 1 byte; length: 2 bytes -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH \ - (CONTROL_MSG_SERIALIZED_MAX_SIZE - 4) +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 4) #define POINTER_ID_MOUSE UINT64_C(-1); @@ -72,7 +71,7 @@ struct control_msg { }; }; -// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE +// buf size must be at least CONTROL_MSG_MAX_SIZE // return the number of bytes written size_t control_msg_serialize(const struct control_msg *msg, unsigned char *buf); diff --git a/app/src/controller.c b/app/src/controller.c index d59a7411..b68b886b 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -60,7 +60,7 @@ controller_push_msg(struct controller *controller, static bool process_msg(struct controller *controller, const struct control_msg *msg) { - unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; int length = control_msg_serialize(msg, serialized_msg); if (!length) { return false; diff --git a/app/src/device_msg.h b/app/src/device_msg.h index f19630bf..60c35817 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -7,9 +7,9 @@ #include "config.h" -#define DEVICE_MSG_SERIALIZED_MAX_SIZE 4096 +#define DEVICE_MSG_MAX_SIZE 4096 // type: 1 byte; length: 2 bytes -#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_SERIALIZED_MAX_SIZE - 3) +#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 3) enum device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, diff --git a/app/src/receiver.c b/app/src/receiver.c index 0474ff55..8c8a7e03 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -60,13 +60,13 @@ static int run_receiver(void *data) { struct receiver *receiver = data; - unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; for (;;) { - assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); + assert(head < DEVICE_MSG_MAX_SIZE); ssize_t r = net_recv(receiver->control_socket, buf, - DEVICE_MSG_SERIALIZED_MAX_SIZE - head); + DEVICE_MSG_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); break; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index c6ff7b2e..a0328060 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -13,7 +13,7 @@ static void test_serialize_inject_keycode(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 10); @@ -34,7 +34,7 @@ static void test_serialize_inject_text(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 16); @@ -54,7 +54,7 @@ static void test_serialize_inject_text_long(void) { text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); @@ -88,7 +88,7 @@ static void test_serialize_inject_touch_event(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 28); @@ -123,7 +123,7 @@ static void test_serialize_inject_scroll_event(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 21); @@ -142,7 +142,7 @@ static void test_serialize_back_or_screen_on(void) { .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -157,7 +157,7 @@ static void test_serialize_expand_notification_panel(void) { .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -172,7 +172,7 @@ static void test_serialize_collapse_notification_panel(void) { .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -187,7 +187,7 @@ static void test_serialize_get_clipboard(void) { .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -206,7 +206,7 @@ static void test_serialize_set_clipboard(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 17); @@ -227,7 +227,7 @@ static void test_serialize_set_screen_power_mode(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 2); @@ -243,7 +243,7 @@ static void test_serialize_rotate_device(void) { .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index 2e29d087..6a38d5e5 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -24,7 +24,7 @@ static void test_deserialize_clipboard(void) { } static void test_deserialize_clipboard_big(void) { - unsigned char input[DEVICE_MSG_SERIALIZED_MAX_SIZE]; + unsigned char input[DEVICE_MSG_MAX_SIZE]; input[0] = DEVICE_MSG_TYPE_CLIPBOARD; input[1] = DEVICE_MSG_TEXT_MAX_LENGTH >> 8; // MSB input[2] = DEVICE_MSG_TEXT_MAX_LENGTH & 0xff; // LSB @@ -33,7 +33,7 @@ static void test_deserialize_clipboard_big(void) { struct device_msg msg; ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); - assert(r == DEVICE_MSG_SERIALIZED_MAX_SIZE); + assert(r == DEVICE_MSG_MAX_SIZE); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); From 245999aec4a4a1454212b38a988aa96fa701bb04 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 21:42:09 +0200 Subject: [PATCH 147/214] Serialize text size on 4 bytes This will allow to send text having a size greater than 65535 bytes. --- app/src/control_msg.c | 6 +++--- app/src/control_msg.h | 4 ++-- app/src/device_msg.c | 10 +++++----- app/src/device_msg.h | 4 ++-- app/tests/test_control_msg_serialize.c | 20 ++++++++++--------- app/tests/test_device_msg_deserialize.c | 12 ++++++----- .../scrcpy/ControlMessageReader.java | 6 +++--- .../scrcpy/DeviceMessageWriter.java | 4 ++-- .../scrcpy/ControlMessageReaderTest.java | 8 ++++---- .../scrcpy/DeviceMessageWriterTest.java | 2 +- 10 files changed, 40 insertions(+), 36 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index c5778c02..2a7f2f34 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -20,9 +20,9 @@ write_position(uint8_t *buf, const struct position *position) { static size_t write_string(const char *utf8, size_t max_len, unsigned char *buf) { size_t len = utf8_truncation_index(utf8, max_len); - buffer_write16be(buf, (uint16_t) len); - memcpy(&buf[2], utf8, len); - return 2 + len; + buffer_write32be(buf, len); + memcpy(&buf[4], utf8, len); + return 4 + len; } static uint16_t diff --git a/app/src/control_msg.h b/app/src/control_msg.h index f5d633a8..d8898c9c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -13,8 +13,8 @@ #define CONTROL_MSG_MAX_SIZE 4096 #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 -// type: 1 byte; paste flag: 1 byte; length: 2 bytes -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 4) +// type: 1 byte; paste flag: 1 byte; length: 4 bytes +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6) #define POINTER_ID_MOUSE UINT64_C(-1); diff --git a/app/src/device_msg.c b/app/src/device_msg.c index db176129..09e68936 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -9,7 +9,7 @@ ssize_t device_msg_deserialize(const unsigned char *buf, size_t len, struct device_msg *msg) { - if (len < 3) { + if (len < 5) { // at least type + empty string length return 0; // not available } @@ -17,8 +17,8 @@ device_msg_deserialize(const unsigned char *buf, size_t len, msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - uint16_t clipboard_len = buffer_read16be(&buf[1]); - if (clipboard_len > len - 3) { + size_t clipboard_len = buffer_read32be(&buf[1]); + if (clipboard_len > len - 5) { return 0; // not available } char *text = SDL_malloc(clipboard_len + 1); @@ -27,12 +27,12 @@ device_msg_deserialize(const unsigned char *buf, size_t len, return -1; } if (clipboard_len) { - memcpy(text, &buf[3], clipboard_len); + memcpy(text, &buf[5], clipboard_len); } text[clipboard_len] = '\0'; msg->clipboard.text = text; - return 3 + clipboard_len; + return 5 + clipboard_len; } default: LOGW("Unknown device message type: %d", (int) msg->type); diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 60c35817..0fc7d87c 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -8,8 +8,8 @@ #include "config.h" #define DEVICE_MSG_MAX_SIZE 4096 -// type: 1 byte; length: 2 bytes -#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 3) +// type: 1 byte; length: 4 bytes +#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) enum device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index a0328060..9ba90ae8 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -36,11 +36,11 @@ static void test_serialize_inject_text(void) { unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 16); + assert(size == 18); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_TEXT, - 0x00, 0x0d, // text length + 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -56,13 +56,15 @@ static void test_serialize_inject_text_long(void) { unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; + unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; - expected[1] = 0x01; - expected[2] = 0x2c; // text length (16 bits) - memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + expected[1] = 0x00; + expected[2] = 0x00; + expected[3] = 0x01; + expected[4] = 0x2c; // text length (32 bits) + memset(&expected[5], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } @@ -208,12 +210,12 @@ static void test_serialize_set_clipboard(void) { unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 17); + assert(size == 19); const unsigned char expected[] = { CONTROL_MSG_TYPE_SET_CLIPBOARD, 1, // paste - 0x00, 0x0d, // text length + 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index 6a38d5e5..8fcfc93d 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -8,13 +8,13 @@ static void test_deserialize_clipboard(void) { const unsigned char input[] = { DEVICE_MSG_TYPE_CLIPBOARD, - 0x00, 0x03, // text length + 0x00, 0x00, 0x00, 0x03, // text length 0x41, 0x42, 0x43, // "ABC" }; struct device_msg msg; ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); - assert(r == 6); + assert(r == 8); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); @@ -26,10 +26,12 @@ static void test_deserialize_clipboard(void) { static void test_deserialize_clipboard_big(void) { unsigned char input[DEVICE_MSG_MAX_SIZE]; input[0] = DEVICE_MSG_TYPE_CLIPBOARD; - input[1] = DEVICE_MSG_TEXT_MAX_LENGTH >> 8; // MSB - input[2] = DEVICE_MSG_TEXT_MAX_LENGTH & 0xff; // LSB + input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; + input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; + input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8; + input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu; - memset(input + 3, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); + memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); struct device_msg msg; ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index b8fba824..a9905919 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -16,7 +16,7 @@ public class ControlMessageReader { private static final int MESSAGE_MAX_SIZE = 4096; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 4; // type: 1 byte; paste flag: 1 byte; length: 2 bytes + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes public static final int INJECT_TEXT_MAX_LENGTH = 300; private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; @@ -103,10 +103,10 @@ public class ControlMessageReader { } private String parseString() { - if (buffer.remaining() < 2) { + if (buffer.remaining() < 4) { return null; } - int len = toUnsigned(buffer.getShort()); + int len = buffer.getInt(); if (buffer.remaining() < len) { return null; } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index ea54d533..2e12698f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -8,7 +8,7 @@ import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { private static final int MESSAGE_MAX_SIZE = 4096; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 3; // type: 1 byte; length: 2 bytes + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); @@ -21,7 +21,7 @@ public class DeviceMessageWriter { String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); - buffer.putShort((short) len); + buffer.putInt(len); buffer.put(raw, 0, len); output.write(rawBuffer, 0, buffer.position()); break; diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index f5fa4d09..c56bd17e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -48,7 +48,7 @@ public class ControlMessageReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -68,7 +68,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; Arrays.fill(text, (byte) 'a'); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -218,7 +218,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); dos.writeByte(1); // paste byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -246,7 +246,7 @@ public class ControlMessageReaderTest { Arrays.fill(rawText, (byte) 'a'); String text = new String(rawText, 0, rawText.length); - dos.writeShort(rawText.length); + dos.writeInt(rawText.length); dos.write(rawText); byte[] packet = bos.toByteArray(); diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java index df12f647..88bf2af9 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -19,7 +19,7 @@ public class DeviceMessageWriterTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); - dos.writeShort(data.length); + dos.writeInt(data.length); dos.write(data); byte[] expected = bos.toByteArray(); From 00d292b2f5833740517cec829cc08531f85155e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 22:44:43 +0200 Subject: [PATCH 148/214] Fix receiver on partial device messages If a single device message was read in several chunks from the control socket, the communication was broken. --- app/src/receiver.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 8c8a7e03..bfe9d6e9 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -65,23 +65,24 @@ run_receiver(void *data) { for (;;) { assert(head < DEVICE_MSG_MAX_SIZE); - ssize_t r = net_recv(receiver->control_socket, buf, + ssize_t r = net_recv(receiver->control_socket, buf + head, DEVICE_MSG_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); break; } - ssize_t consumed = process_msgs(buf, r); + head += r; + ssize_t consumed = process_msgs(buf, head); if (consumed == -1) { // an error occurred break; } if (consumed) { + head -= consumed; // shift the remaining data in the buffer - memmove(buf, &buf[consumed], r - consumed); - head = r - consumed; + memmove(buf, &buf[consumed], head); } } From 488d22d4e23afe902bbcef0e3c535591f66b6f2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 23:23:09 +0200 Subject: [PATCH 149/214] Increase clipboard size from 4k to 256k Beyond 256k, SDL_GetClipboardText() returns an empty string on my computer. Fixes #1117 --- app/src/control_msg.h | 2 +- app/src/device_msg.h | 2 +- .../main/java/com/genymobile/scrcpy/ControlMessageReader.java | 2 +- .../main/java/com/genymobile/scrcpy/DeviceMessageWriter.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/control_msg.h b/app/src/control_msg.h index d8898c9c..bc4ff9ef 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,7 +10,7 @@ #include "android/keycodes.h" #include "common.h" -#define CONTROL_MSG_MAX_SIZE 4096 +#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 // type: 1 byte; paste flag: 1 byte; length: 4 bytes diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 0fc7d87c..4b681e2c 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -7,7 +7,7 @@ #include "config.h" -#define DEVICE_MSG_MAX_SIZE 4096 +#define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k // type: 1 byte; length: 4 bytes #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index a9905919..3b3e9559 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -14,7 +14,7 @@ public class ControlMessageReader { static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; - private static final int MESSAGE_MAX_SIZE = 4096; + private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes public static final int INJECT_TEXT_MAX_LENGTH = 300; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index 2e12698f..15d91a35 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -7,7 +7,7 @@ import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { - private static final int MESSAGE_MAX_SIZE = 4096; + private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; From 1e4ee547b560ad3c52ce33fd32cf1b10b3d24d1a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Jun 2020 23:50:08 +0200 Subject: [PATCH 150/214] Make message buffer static Now that the message max size is 256k, do not put the buffer on the stack. --- app/src/controller.c | 2 +- app/src/receiver.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index b68b886b..c5897e5d 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -60,7 +60,7 @@ controller_push_msg(struct controller *controller, static bool process_msg(struct controller *controller, const struct control_msg *msg) { - unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; + static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; int length = control_msg_serialize(msg, serialized_msg); if (!length) { return false; diff --git a/app/src/receiver.c b/app/src/receiver.c index bfe9d6e9..5ecff3b7 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -60,7 +60,7 @@ static int run_receiver(void *data) { struct receiver *receiver = data; - unsigned char buf[DEVICE_MSG_MAX_SIZE]; + static unsigned char buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; for (;;) { From 42641d273772b2dcc55a34f8ae7cd5f6bdea51d5 Mon Sep 17 00:00:00 2001 From: AreYouLoco? Date: Thu, 18 Jun 2020 20:05:42 +0200 Subject: [PATCH 151/214] Fix typo in README.md PR #1523 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df3e1172..a894313b 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer From another terminal: ```bash -scrcpy --force-adb-forwrad +scrcpy --force-adb-forward ``` From dc7b60e6199b90a45ea26751988f6f30f8b2efdf Mon Sep 17 00:00:00 2001 From: Ivan Keliukh Date: Sat, 13 Jun 2020 15:10:41 +0300 Subject: [PATCH 152/214] Add option for disabling screensaver PR #1502 Fixes #1370 Signed-off-by: Romain Vimont --- README.md | 11 +++++++++++ app/scrcpy.1 | 4 ++++ app/src/cli.c | 9 +++++++++ app/src/scrcpy.c | 15 +++++++++++---- app/src/scrcpy.h | 2 ++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fe7f0fac..2c01a01e 100644 --- a/README.md +++ b/README.md @@ -479,6 +479,17 @@ scrcpy -t Note that it only shows _physical_ touches (with the finger on the device). +#### Disable screensaver + +By default, scrcpy does not prevent the screensaver to run on the computer. + +To disable it: + +```bash +scrcpy --disable-screensaver +``` + + ### Input control #### Rotate device screen diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 776d78ae..4e3c43f2 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -43,6 +43,10 @@ The values are expressed in the device natural orientation (typically, portrait .B \-\-max\-size value is computed on the cropped size. +.TP +.BI "\-\-disable-screensaver" +Disable screensaver while scrcpy is running. + .TP .BI "\-\-display " id Specify the display id to mirror. diff --git a/app/src/cli.c b/app/src/cli.c index be0b7c23..6a45d430 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -45,6 +45,9 @@ scrcpy_print_usage(const char *arg0) { " (typically, portrait for a phone, landscape for a tablet).\n" " Any --max-size value is computed on the cropped size.\n" "\n" + " --disable-screensaver\n" + " Disable screensaver while scrcpy is running.\n" + "\n" " --display id\n" " Specify the display id to mirror.\n" "\n" @@ -526,6 +529,7 @@ guess_record_format(const char *filename) { #define OPT_NO_MIPMAPS 1017 #define OPT_CODEC_OPTIONS 1018 #define OPT_FORCE_ADB_FORWARD 1019 +#define OPT_DISABLE_SCREENSAVER 1020 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -534,6 +538,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"bit-rate", required_argument, NULL, 'b'}, {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, {"crop", required_argument, NULL, OPT_CROP}, + {"disable-screensaver", no_argument, NULL, + OPT_DISABLE_SCREENSAVER}, {"display", required_argument, NULL, OPT_DISPLAY_ID}, {"force-adb-forward", no_argument, NULL, OPT_FORCE_ADB_FORWARD}, @@ -716,6 +722,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_FORCE_ADB_FORWARD: opts->force_adb_forward = true; break; + case OPT_DISABLE_SCREENSAVER: + opts->disable_screensaver = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 67ebf8c0..cde6c27f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -63,7 +63,8 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { // init SDL and set appropriate hints static bool -sdl_init_and_configure(bool display, const char *render_driver) { +sdl_init_and_configure(bool display, const char *render_driver, + bool disable_screensaver) { uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; if (SDL_Init(flags)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); @@ -112,8 +113,13 @@ sdl_init_and_configure(bool display, const char *render_driver) { LOGW("Could not disable minimize on focus loss"); } - // Do not disable the screensaver when scrcpy is running - SDL_EnableScreenSaver(); + if (disable_screensaver) { + LOGD("Screensaver disabled"); + SDL_DisableScreenSaver(); + } else { + LOGD("Screensaver enabled"); + SDL_EnableScreenSaver(); + } return true; } @@ -321,7 +327,8 @@ scrcpy(const struct scrcpy_options *options) { bool controller_initialized = false; bool controller_started = false; - if (!sdl_init_and_configure(options->display, options->render_driver)) { + if (!sdl_init_and_configure(options->display, options->render_driver, + options->disable_screensaver)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 70d99433..f2760152 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -43,6 +43,7 @@ struct scrcpy_options { bool mipmaps; bool stay_awake; bool force_adb_forward; + bool disable_screensaver; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -81,6 +82,7 @@ struct scrcpy_options { .mipmaps = true, \ .stay_awake = false, \ .force_adb_forward = false, \ + .disable_screensaver = false, \ } bool From 29e5af76d4f0a7d5b079c760cc48aa8b26c554e8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Jun 2020 21:54:24 +0200 Subject: [PATCH 153/214] Remove fprintf() call in tests It should never have been committed. --- app/tests/test_cli.c | 1 - 1 file changed, 1 deletion(-) diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index c5d95633..2a1e2607 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -73,7 +73,6 @@ static void test_options(void) { const struct scrcpy_options *opts = &args.opts; assert(opts->always_on_top); - fprintf(stderr, "%d\n", (int) opts->bit_rate); assert(opts->bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); From 0ba74fbd9a1c599b7a93d44631e6973eb4940049 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Jun 2020 22:04:06 +0200 Subject: [PATCH 154/214] Make scrcpy.h independant of other headers The header scrcpy.h is intended to be the "public" API. It should not depend on other internal headers. Therefore, declare all required structs in this header and adapt internal code. --- app/src/cli.c | 21 +++++++++++---------- app/src/main.c | 1 + app/src/recorder.c | 8 ++++---- app/src/recorder.h | 11 +++-------- app/src/scrcpy.c | 2 ++ app/src/scrcpy.h | 39 ++++++++++++++++++++++++++++----------- app/src/screen.c | 5 +++-- app/src/screen.h | 4 +--- app/src/server.c | 6 +++--- app/src/server.h | 5 +++-- app/src/util/log.h | 7 ------- app/tests/test_cli.c | 5 +++-- 12 files changed, 62 insertions(+), 52 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 6a45d430..9e07e912 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -3,10 +3,11 @@ #include #include #include +#include #include #include "config.h" -#include "recorder.h" +#include "scrcpy.h" #include "util/log.h" #include "util/str_util.h" @@ -382,10 +383,10 @@ parse_rotation(const char *s, uint8_t *rotation) { static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" - static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); + static_assert(SC_WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); if (!strcmp(s, "auto")) { - *position = WINDOW_POSITION_UNDEFINED; + *position = SC_WINDOW_POSITION_UNDEFINED; return true; } @@ -414,7 +415,7 @@ parse_window_dimension(const char *s, uint16_t *dimension) { } static bool -parse_port_range(const char *s, struct port_range *port_range) { +parse_port_range(const char *s, struct sc_port_range *port_range) { long values[2]; size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); if (!count) { @@ -480,20 +481,20 @@ parse_log_level(const char *s, enum sc_log_level *log_level) { } static bool -parse_record_format(const char *optarg, enum recorder_format *format) { +parse_record_format(const char *optarg, enum sc_record_format *format) { if (!strcmp(optarg, "mp4")) { - *format = RECORDER_FORMAT_MP4; + *format = SC_RECORD_FORMAT_MP4; return true; } if (!strcmp(optarg, "mkv")) { - *format = RECORDER_FORMAT_MKV; + *format = SC_RECORD_FORMAT_MKV; return true; } LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); return false; } -static enum recorder_format +static enum sc_record_format guess_record_format(const char *filename) { size_t len = strlen(filename); if (len < 4) { @@ -501,10 +502,10 @@ guess_record_format(const char *filename) { } const char *ext = &filename[len - 4]; if (!strcmp(ext, ".mp4")) { - return RECORDER_FORMAT_MP4; + return SC_RECORD_FORMAT_MP4; } if (!strcmp(ext, ".mkv")) { - return RECORDER_FORMAT_MKV; + return SC_RECORD_FORMAT_MKV; } return 0; } diff --git a/app/src/main.c b/app/src/main.c index 7d7ddb82..202c217c 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,5 +1,6 @@ #include "scrcpy.h" +#include #include #include #include diff --git a/app/src/recorder.c b/app/src/recorder.c index 465b24e8..76edbd03 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -63,7 +63,7 @@ recorder_queue_clear(struct recorder_queue *queue) { bool recorder_init(struct recorder *recorder, const char *filename, - enum recorder_format format, + enum sc_record_format format, struct size declared_frame_size) { recorder->filename = SDL_strdup(filename); if (!recorder->filename) { @@ -105,10 +105,10 @@ recorder_destroy(struct recorder *recorder) { } static const char * -recorder_get_format_name(enum recorder_format format) { +recorder_get_format_name(enum sc_record_format format) { switch (format) { - case RECORDER_FORMAT_MP4: return "mp4"; - case RECORDER_FORMAT_MKV: return "matroska"; + case SC_RECORD_FORMAT_MP4: return "mp4"; + case SC_RECORD_FORMAT_MKV: return "matroska"; default: return NULL; } } diff --git a/app/src/recorder.h b/app/src/recorder.h index 4f5d526c..bc87a23b 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -8,14 +8,9 @@ #include "config.h" #include "common.h" +#include "scrcpy.h" #include "util/queue.h" -enum recorder_format { - RECORDER_FORMAT_AUTO, - RECORDER_FORMAT_MP4, - RECORDER_FORMAT_MKV, -}; - struct record_packet { AVPacket packet; struct record_packet *next; @@ -25,7 +20,7 @@ struct recorder_queue QUEUE(struct record_packet); struct recorder { char *filename; - enum recorder_format format; + enum sc_record_format format; AVFormatContext *ctx; struct size declared_frame_size; bool header_written; @@ -46,7 +41,7 @@ struct recorder { bool recorder_init(struct recorder *recorder, const char *filename, - enum recorder_format format, struct size declared_frame_size); + enum sc_record_format format, struct size declared_frame_size); void recorder_destroy(struct recorder *recorder); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index cde6c27f..5a88122d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -8,6 +8,8 @@ #include #ifdef _WIN32 +// not needed here, but winsock2.h must never be included AFTER windows.h +# include # include #endif diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index f2760152..d7eb2f58 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -2,13 +2,30 @@ #define SCRCPY_H #include +#include #include #include "config.h" -#include "common.h" -#include "input_manager.h" -#include "recorder.h" -#include "util/log.h" + +enum sc_log_level { + SC_LOG_LEVEL_DEBUG, + SC_LOG_LEVEL_INFO, + SC_LOG_LEVEL_WARN, + SC_LOG_LEVEL_ERROR, +}; + +enum sc_record_format { + SC_RECORD_FORMAT_AUTO, + SC_RECORD_FORMAT_MP4, + SC_RECORD_FORMAT_MKV, +}; + +struct sc_port_range { + uint16_t first; + uint16_t last; +}; + +#define SC_WINDOW_POSITION_UNDEFINED (-0x8000) struct scrcpy_options { const char *serial; @@ -19,15 +36,15 @@ struct scrcpy_options { const char *render_driver; const char *codec_options; enum sc_log_level log_level; - enum recorder_format record_format; - struct port_range port_range; + enum sc_record_format record_format; + struct sc_port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; int8_t lock_video_orientation; uint8_t rotation; - int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto" - int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" + int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" + int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; uint16_t window_height; uint16_t display_id; @@ -55,7 +72,7 @@ struct scrcpy_options { .render_driver = NULL, \ .codec_options = NULL, \ .log_level = SC_LOG_LEVEL_INFO, \ - .record_format = RECORDER_FORMAT_AUTO, \ + .record_format = SC_RECORD_FORMAT_AUTO, \ .port_range = { \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ @@ -65,8 +82,8 @@ struct scrcpy_options { .max_fps = 0, \ .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ .rotation = 0, \ - .window_x = WINDOW_POSITION_UNDEFINED, \ - .window_y = WINDOW_POSITION_UNDEFINED, \ + .window_x = SC_WINDOW_POSITION_UNDEFINED, \ + .window_y = SC_WINDOW_POSITION_UNDEFINED, \ .window_width = 0, \ .window_height = 0, \ .display_id = 0, \ diff --git a/app/src/screen.c b/app/src/screen.c index 66a1163b..4cee61b0 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -8,6 +8,7 @@ #include "common.h" #include "compat.h" #include "icon.xpm" +#include "scrcpy.h" #include "tiny_xpm.h" #include "video_buffer.h" #include "util/lock.h" @@ -257,9 +258,9 @@ screen_init_rendering(struct screen *screen, const char *window_title, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != WINDOW_POSITION_UNDEFINED + int x = window_x != SC_WINDOW_POSITION_UNDEFINED ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = window_y != WINDOW_POSITION_UNDEFINED + int y = window_y != SC_WINDOW_POSITION_UNDEFINED ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, diff --git a/app/src/screen.h b/app/src/screen.h index aa6218f7..4c3a593e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -9,8 +9,6 @@ #include "common.h" #include "opengl.h" -#define WINDOW_POSITION_UNDEFINED (-0x8000) - struct video_buffer; struct screen { @@ -76,7 +74,7 @@ void screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) -// window_x and window_y accept WINDOW_POSITION_UNDEFINED +// window_x and window_y accept SC_WINDOW_POSITION_UNDEFINED bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, diff --git a/app/src/server.c b/app/src/server.c index fb498d63..05b2cf91 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -143,7 +143,7 @@ listen_on_port(uint16_t port) { static bool enable_tunnel_reverse_any_port(struct server *server, - struct port_range port_range) { + struct sc_port_range port_range) { uint16_t port = port_range.first; for (;;) { if (!enable_tunnel_reverse(server->serial, port)) { @@ -189,7 +189,7 @@ enable_tunnel_reverse_any_port(struct server *server, static bool enable_tunnel_forward_any_port(struct server *server, - struct port_range port_range) { + struct sc_port_range port_range) { server->tunnel_forward = true; uint16_t port = port_range.first; for (;;) { @@ -217,7 +217,7 @@ enable_tunnel_forward_any_port(struct server *server, } static bool -enable_tunnel_any_port(struct server *server, struct port_range port_range, +enable_tunnel_any_port(struct server *server, struct sc_port_range port_range, bool force_adb_forward) { if (!force_adb_forward) { // Attempt to use "adb reverse" diff --git a/app/src/server.h b/app/src/server.h index 2215d817..254afe30 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -9,6 +9,7 @@ #include "config.h" #include "command.h" #include "common.h" +#include "scrcpy.h" #include "util/log.h" #include "util/net.h" @@ -20,7 +21,7 @@ struct server { socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; - struct port_range port_range; + struct sc_port_range port_range; uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" @@ -47,7 +48,7 @@ struct server_params { enum sc_log_level log_level; const char *crop; const char *codec_options; - struct port_range port_range; + struct sc_port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; diff --git a/app/src/util/log.h b/app/src/util/log.h index 8c5c7dee..5955c7fb 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -3,13 +3,6 @@ #include -enum sc_log_level { - SC_LOG_LEVEL_DEBUG, - SC_LOG_LEVEL_INFO, - SC_LOG_LEVEL_WARN, - SC_LOG_LEVEL_ERROR, -}; - #define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 2a1e2607..07974361 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -1,4 +1,5 @@ #include +#include #include "cli.h" #include "common.h" @@ -83,7 +84,7 @@ static void test_options(void) { assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); - assert(opts->record_format == RECORDER_FORMAT_MKV); + assert(opts->record_format == SC_RECORD_FORMAT_MKV); assert(opts->render_expired_frames); assert(!strcmp(opts->serial, "0123456789abcdef")); assert(opts->show_touches); @@ -118,7 +119,7 @@ static void test_options2(void) { assert(!opts->control); assert(!opts->display); assert(!strcmp(opts->record_filename, "file.mp4")); - assert(opts->record_format == RECORDER_FORMAT_MP4); + assert(opts->record_format == SC_RECORD_FORMAT_MP4); } int main(void) { From 3c1ed5d86c38bcbd5353b0a7e6ef5653d6c44d0d Mon Sep 17 00:00:00 2001 From: xeropresence <3128949+xeropresence@users.noreply.github.com> Date: Thu, 11 Jun 2020 04:40:52 -0400 Subject: [PATCH 155/214] Handle repeating keycodes Initialize "repeat" count on key events. PR #1519 Refs #1013 Signed-off-by: Romain Vimont --- app/src/control_msg.c | 5 +++-- app/src/control_msg.h | 1 + app/src/input_manager.c | 11 +++++++++-- app/src/input_manager.h | 5 +++++ app/src/scrcpy.c | 1 + app/tests/test_control_msg_serialize.c | 4 +++- .../java/com/genymobile/scrcpy/ControlMessage.java | 8 +++++++- .../com/genymobile/scrcpy/ControlMessageReader.java | 5 +++-- .../main/java/com/genymobile/scrcpy/Controller.java | 6 +++--- .../genymobile/scrcpy/ControlMessageReaderTest.java | 10 ++++++++++ 10 files changed, 45 insertions(+), 11 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 2a7f2f34..27c7903d 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -42,8 +42,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_INJECT_KEYCODE: buf[1] = msg->inject_keycode.action; buffer_write32be(&buf[2], msg->inject_keycode.keycode); - buffer_write32be(&buf[6], msg->inject_keycode.metastate); - return 10; + buffer_write32be(&buf[6], msg->inject_keycode.repeat); + buffer_write32be(&buf[10], msg->inject_keycode.metastate); + return 14; case CONTROL_MSG_TYPE_INJECT_TEXT: { size_t len = write_string(msg->inject_text.text, diff --git a/app/src/control_msg.h b/app/src/control_msg.h index bc4ff9ef..e0b480de 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -44,6 +44,7 @@ struct control_msg { struct { enum android_keyevent_action action; enum android_keycode keycode; + uint32_t repeat; enum android_metastate metastate; } inject_keycode; struct { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 54e619bf..52f4f9fe 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -234,7 +234,7 @@ input_manager_process_text_input(struct input_manager *im, static bool convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, - bool prefer_text) { + bool prefer_text, uint32_t repeat) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { @@ -247,6 +247,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, return false; } + to->inject_keycode.repeat = repeat; to->inject_keycode.metastate = convert_meta_state(mod); return true; @@ -411,8 +412,14 @@ input_manager_process_key(struct input_manager *im, return; } + if (event->repeat) { + ++im->repeat; + } else { + im->repeat = 0; + } + struct control_msg msg; - if (convert_input_key(event, &msg, im->prefer_text)) { + if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) { if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject keycode'"); } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 43fc0eeb..90b74fec 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -14,6 +14,11 @@ struct input_manager { struct controller *controller; struct video_buffer *video_buffer; struct screen *screen; + + // SDL reports repeated events as a boolean, but Android expects the actual + // number of repetitions. This variable keeps track of the count. + unsigned repeat; + bool prefer_text; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5a88122d..889cbb87 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -48,6 +48,7 @@ static struct input_manager input_manager = { .controller = &controller, .video_buffer = &video_buffer, .screen = &screen, + .repeat = 0, .prefer_text = false, // initialized later }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 9ba90ae8..592c2628 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -9,18 +9,20 @@ static void test_serialize_inject_keycode(void) { .inject_keycode = { .action = AKEY_EVENT_ACTION_UP, .keycode = AKEYCODE_ENTER, + .repeat = 5, .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 10); + assert(size == 14); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER + 0x00, 0x00, 0x00, 0X05, // repeat 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON }; assert(!memcmp(buf, expected, sizeof(expected))); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 7d0ab7a6..dbb8d382 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -31,15 +31,17 @@ public final class ControlMessage { private int hScroll; private int vScroll; private int flags; + private int repeat; private ControlMessage() { } - public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { + public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_KEYCODE; msg.action = action; msg.keycode = keycode; + msg.repeat = repeat; msg.metaState = metaState; return msg; } @@ -144,4 +146,8 @@ public final class ControlMessage { public int getFlags() { return flags; } + + public int getRepeat() { + return repeat; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 3b3e9559..132e3f4e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -8,7 +8,7 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { - static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; @@ -98,8 +98,9 @@ public class ControlMessageReader { } int action = toUnsigned(buffer.get()); int keycode = buffer.getInt(); + int repeat = buffer.getInt(); int metaState = buffer.getInt(); - return ControlMessage.createInjectKeycode(action, keycode, metaState); + return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } private String parseString() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 960c6a6e..bc4a0601 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -74,7 +74,7 @@ public class Controller { switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: if (device.supportsInputEvents()) { - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); } break; case ControlMessage.TYPE_INJECT_TEXT: @@ -130,8 +130,8 @@ public class Controller { } } - private boolean injectKeycode(int action, int keycode, int metaState) { - return device.injectKeyEvent(action, keycode, 0, metaState); + private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { + return device.injectKeyEvent(action, keycode, repeat, metaState); } private boolean injectChar(char c) { diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index c56bd17e..1e2b8266 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -25,6 +25,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); @@ -37,6 +38,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } @@ -308,11 +310,13 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(0); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(1); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); @@ -322,12 +326,14 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(0, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(1, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } @@ -341,6 +347,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(4); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); @@ -353,6 +360,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(4, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); @@ -360,6 +368,7 @@ public class ControlMessageReaderTest { bos.reset(); dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); @@ -369,6 +378,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } } From 8c27f59aa571504e2e038dc5386e2be6897cec32 Mon Sep 17 00:00:00 2001 From: Hossam Elbadissi Date: Thu, 25 Jun 2020 19:38:19 +0100 Subject: [PATCH 156/214] Improve linguistic PR #1543 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a894313b..847a744d 100644 --- a/README.md +++ b/README.md @@ -441,7 +441,7 @@ Or by pressing `Ctrl`+`o` at any time. To turn it back on, press `Ctrl`+`Shift`+`o` (or `POWER`, `Ctrl`+`p`). -It can be useful to also prevent the device to sleep: +It can also be useful to prevent the device from sleeping: ```bash scrcpy --turn-screen-off --stay-awake From e8a565f9ea91a30cd354de696b3b35ea74cda83b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jun 2020 08:54:40 +0200 Subject: [PATCH 157/214] Fix touch events HiDPI-scaling Touch events were HiDPI-scaled twice: - once because the position (provided as floats between 0 and 1) were converted in pixels using the drawable size (not the window size) - once due to screen_convert_to_frame_coords() One possible fix could be to compute the position in pixels from the window size instead, but this would unnecessarily round the event position to the nearest window coordinates (instead of drawable coordinates). Instead, expose two separate functions to convert to frame coordinates from either window or drawable coordinates. Fixes #1536 Refs #15 Refs e40532a3761da8b3aef484f1d435ef5f50d94574 --- app/src/input_manager.c | 19 ++++++++++--------- app/src/screen.c | 11 +++++++++-- app/src/screen.h | 9 ++++++++- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 52f4f9fe..9c22ee0a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -434,7 +434,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = - screen_convert_to_frame_coords(screen, from->x, from->y); + screen_convert_window_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(from->state); @@ -472,15 +472,15 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = from->fingerId; to->inject_touch_event.position.screen_size = screen->frame_size; - int ww; - int wh; - SDL_GL_GetDrawableSize(screen->window, &ww, &wh); + int dw; + int dh; + SDL_GL_GetDrawableSize(screen->window, &dw, &dh); // SDL touch event coordinates are normalized in the range [0; 1] - int32_t x = from->x * ww; - int32_t y = from->y * wh; + int32_t x = from->x * dw; + int32_t y = from->y * dh; to->inject_touch_event.position.point = - screen_convert_to_frame_coords(screen, x, y); + screen_convert_drawable_to_frame_coords(screen, x, y); to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; @@ -510,7 +510,7 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = - screen_convert_to_frame_coords(screen, from->x, from->y); + screen_convert_window_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); @@ -575,7 +575,8 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, struct position position = { .screen_size = screen->frame_size, - .point = screen_convert_to_frame_coords(screen, mouse_x, mouse_y), + .point = screen_convert_window_to_frame_coords(screen, + mouse_x, mouse_y), }; to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; diff --git a/app/src/screen.c b/app/src/screen.c index 4cee61b0..fe2bc867 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -580,14 +580,14 @@ screen_handle_window_event(struct screen *screen, } struct point -screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { +screen_convert_drawable_to_frame_coords(struct screen *screen, + int32_t x, int32_t y) { unsigned rotation = screen->rotation; assert(rotation < 4); int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; - screen_hidpi_scale_coords(screen, &x, &y); x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; @@ -616,6 +616,13 @@ screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { return result; } +struct point +screen_convert_window_to_frame_coords(struct screen *screen, + int32_t x, int32_t y) { + screen_hidpi_scale_coords(screen, &x, &y); + return screen_convert_drawable_to_frame_coords(screen, x, y); +} + void screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) { // take the HiDPI scaling (dw/ww and dh/wh) into account diff --git a/app/src/screen.h b/app/src/screen.h index 4c3a593e..c4fbbf66 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -124,7 +124,14 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); // convert point from window coordinates to frame coordinates // x and y are expressed in pixels struct point -screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y); +screen_convert_window_to_frame_coords(struct screen *screen, + int32_t x, int32_t y); + +// convert point from drawable coordinates to frame coordinates +// x and y are expressed in pixels +struct point +screen_convert_drawable_to_frame_coords(struct screen *screen, + int32_t x, int32_t y); // Convert coordinates from window to drawable. // Events are expressed in window coordinates, but content is expressed in From e99b896ae2b6a59d5f7f2b76a3dd9c29f4838013 Mon Sep 17 00:00:00 2001 From: axxx007xxxz Date: Sat, 27 Jun 2020 12:16:27 +0200 Subject: [PATCH 158/214] README: Add Fedora install instructions PR #1549 Signed-off-by: Romain Vimont --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 847a744d..5818b95f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,11 @@ A [Snap] package is available: [`scrcpy`][snap-link]. [snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) +For Fedora, a [COPR] package is available: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link]. [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository From f7d4b6d0db0f417ab0c45088710dc174e97a0894 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Jul 2020 08:50:52 +0200 Subject: [PATCH 159/214] Do not crash on missing clipboard manager Some devices have no clipboard manager. In that case, do not try to enable clipboard synchronization to avoid a crash. Fixes #1440 Fixes #1556 --- .../java/com/genymobile/scrcpy/Device.java | 46 ++++++++++++------- .../scrcpy/wrappers/ServiceManager.java | 9 +++- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 349486c3..14495736 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -80,23 +81,28 @@ public final class Device { if (options.getControl()) { // If control is enabled, synchronize Android clipboard to the computer automatically - serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { - @Override - public void dispatchPrimaryClipChanged() { - if (isSettingClipboard.get()) { - // This is a notification for the change we are currently applying, ignore it - return; - } - synchronized (Device.this) { - if (clipboardListener != null) { - String text = getClipboardText(); - if (text != null) { - clipboardListener.onClipboardTextChanged(text); + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager != null) { + clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { + @Override + public void dispatchPrimaryClipChanged() { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + synchronized (Device.this) { + if (clipboardListener != null) { + String text = getClipboardText(); + if (text != null) { + clipboardListener.onClipboardTextChanged(text); + } } } } - } - }); + }); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); + } } if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { @@ -199,7 +205,11 @@ public final class Device { } public String getClipboardText() { - CharSequence s = serviceManager.getClipboardManager().getText(); + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager == null) { + return null; + } + CharSequence s = clipboardManager.getText(); if (s == null) { return null; } @@ -207,8 +217,12 @@ public final class Device { } public boolean setClipboardText(String text) { + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager == null) { + return false; + } isSettingClipboard.set(true); - boolean ok = serviceManager.getClipboardManager().setText(text); + boolean ok = clipboardManager.setText(text); isSettingClipboard.set(false); return ok; } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index c4ce59c2..6f4b9c04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -77,7 +77,14 @@ public final class ServiceManager { public ClipboardManager getClipboardManager() { if (clipboardManager == null) { - clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); + IInterface clipboard = getService("clipboard", "android.content.IClipboard"); + if (clipboard == null) { + // Some devices have no clipboard manager + // + // + return null; + } + clipboardManager = new ClipboardManager(clipboard); } return clipboardManager; } From 334964c380ebc647b9f1e876182372efa51f26dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Jul 2020 15:34:34 +0200 Subject: [PATCH 160/214] Make setScreenPowerMode() method static Its implementation does not use the instance at all. --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 2 +- server/src/main/java/com/genymobile/scrcpy/Device.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index bc4a0601..71e7ec9c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -116,7 +116,7 @@ public class Controller { case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { int mode = msg.getAction(); - boolean setPowerModeOk = device.setScreenPowerMode(mode); + boolean setPowerModeOk = Device.setScreenPowerMode(mode); if (setPowerModeOk) { Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 14495736..6ba8d446 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -230,7 +230,7 @@ public final class Device { /** * @param mode one of the {@code SCREEN_POWER_MODE_*} constants */ - public boolean setScreenPowerMode(int mode) { + public static boolean setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); From 5fa46ad0c71169f9369f9f3bde9cce3d29ed4b88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Jul 2020 15:46:50 +0200 Subject: [PATCH 161/214] Fix constants name in comment --- server/src/main/java/com/genymobile/scrcpy/Device.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 6ba8d446..375c9ed6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -228,7 +228,7 @@ public final class Device { } /** - * @param mode one of the {@code SCREEN_POWER_MODE_*} constants + * @param mode one of the {@code POWER_MODE_*} constants */ public static boolean setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(); From 5086e7b744e9edfa9d1d56cf5ae2ad3b0ae32ddf Mon Sep 17 00:00:00 2001 From: NGAU Zeonfung Date: Sat, 4 Jul 2020 02:27:21 +0900 Subject: [PATCH 162/214] Update BUILD.md Update the macOS section. PR #1559 Signed-off-by: Romain Vimont --- BUILD.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index 3696e6b0..57b2db97 100644 --- a/BUILD.md +++ b/BUILD.md @@ -176,8 +176,8 @@ Additionally, if you want to build the server, install Java 8 from Caskroom, and make it avaliable from the `PATH`: ```bash -brew tap caskroom/versions -brew cask install java8 +brew tap homebrew/cask-versions +brew cask install adoptopenjdk/openjdk/adoptopenjdk8 export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)" export PATH="$JAVA_HOME/bin:$PATH" ``` @@ -190,12 +190,17 @@ See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker). ## Common steps If you want to build the server, install the [Android SDK] (_Android Studio_), -and set `ANDROID_HOME` to its directory. For example: +and set `ANDROID_SDK_ROOT` to its directory. For example: [Android SDK]: https://developer.android.com/studio/index.html ```bash -export ANDROID_HOME=~/android/sdk +# Linux +export ANDROID_SDK_ROOT=~/Android/Sdk +# Mac +export ANDROID_SDK_ROOT=~/Library/Android/sdk +# Windows +set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk ``` If you don't want to build the server, use the [prebuilt server]. From deea29f52ac742a5090b53bc880e113bfbbcb265 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Jul 2020 19:37:52 +0200 Subject: [PATCH 163/214] Send touch event without pressure on button up Refs #1518 --- app/src/input_manager.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 9c22ee0a..7d500efe 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -511,7 +511,8 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = screen_convert_window_to_frame_coords(screen, from->x, from->y); - to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.pressure = + from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f; to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); From a973757fd141fc07ef81314d350bd4f4de64f59c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Jul 2020 22:31:05 +0200 Subject: [PATCH 164/214] Warn on ignored touch event In theory, this was expected to only happen when a touch event is sent just before the device is rotated, but some devices do not respect the encoding size, causing an unexpected mismatch. Refs #1518 --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 71e7ec9c..abfc434a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -166,7 +166,7 @@ public class Controller { Point point = device.getPhysicalPoint(position); if (point == null) { - // ignore event + Ln.w("Ignore touch event, it was generated for a different device size"); return false; } From 30714aba34e8b447159beaebeaf4eeab7bd33907 Mon Sep 17 00:00:00 2001 From: brunoais Date: Tue, 7 Jul 2020 22:13:15 +0100 Subject: [PATCH 165/214] Restore power mode to normal on cleanup This avoids to let the device screen turned off (as enabled by Ctrl+o or --turn-screen-off). PR #1576 Fixes #1572 --- .../java/com/genymobile/scrcpy/CleanUp.java | 17 ++++++++++++----- .../main/java/com/genymobile/scrcpy/Server.java | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 74555636..d0ea141b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -19,18 +19,19 @@ public final class CleanUp { // not instantiable } - public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException { - boolean needProcess = disableShowTouches || restoreStayOn != -1; + public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException { + boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode; if (needProcess) { - startProcess(disableShowTouches, restoreStayOn); + startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode); } else { // There is no additional clean up to do when scrcpy dies unlinkSelf(); } } - private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)}; + private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf( + restoreStayOn), String.valueOf(restoreNormalPowerMode)}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", SERVER_PATH); @@ -59,6 +60,7 @@ public final class CleanUp { boolean disableShowTouches = Boolean.parseBoolean(args[0]); int restoreStayOn = Integer.parseInt(args[1]); + boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]); if (disableShowTouches || restoreStayOn != -1) { ServiceManager serviceManager = new ServiceManager(); @@ -73,5 +75,10 @@ public final class CleanUp { } } } + + if (restoreNormalPowerMode) { + Ln.i("Restoring normal power mode"); + Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 44b3afd4..d257e319 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -49,7 +49,7 @@ public final class Server { } } - CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); + CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true); boolean tunnelForward = options.isTunnelForward(); From 322f1512ea806611eb37f508cd952bbbc050f853 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Jul 2020 10:21:24 +0200 Subject: [PATCH 166/214] Inject WAKEUP instead of POWER To power the device on, inject KEYCODE_WAKEUP to avoid a possible race condition (the device might become off between the test isScreenOn() and the POWER keycode injection). --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index abfc434a..6ff8d208 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -48,10 +48,10 @@ public class Controller { public void control() throws IOException { // on start, power on the device if (!device.isScreenOn()) { - device.injectKeycode(KeyEvent.KEYCODE_POWER); + device.injectKeycode(KeyEvent.KEYCODE_WAKEUP); // dirty hack - // After POWER is injected, the device is powered on asynchronously. + // After the keycode is injected, the device is powered on asynchronously. // To turn the device screen off while mirroring, the client will send a message that // would be handled before the device is actually powered on, so its effect would // be "canceled" once the device is turned back on. @@ -225,7 +225,7 @@ public class Controller { } private boolean pressBackOrTurnScreenOn() { - int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; + int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_WAKEUP; return device.injectKeycode(keycode); } From 199c74f62f5f78748769e6489122242a3217dc54 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Jul 2020 12:17:04 +0200 Subject: [PATCH 167/214] Declare main() with argc/argv params in tests Declaring the main method as "int main(void)" causes issues with SDL. Fixes #1209 --- app/tests/test_buffer_util.c | 5 ++++- app/tests/test_cbuf.c | 5 ++++- app/tests/test_cli.c | 5 ++++- app/tests/test_control_msg_serialize.c | 5 ++++- app/tests/test_device_msg_deserialize.c | 5 ++++- app/tests/test_queue.c | 5 ++++- app/tests/test_strutil.c | 5 ++++- 7 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/tests/test_buffer_util.c b/app/tests/test_buffer_util.c index ba3f9f06..d61e6918 100644 --- a/app/tests/test_buffer_util.c +++ b/app/tests/test_buffer_util.c @@ -65,7 +65,10 @@ static void test_buffer_read64be(void) { assert(val == 0xABCD1234567890EF); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_buffer_write16be(); test_buffer_write32be(); test_buffer_write64be(); diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c index dbe50aab..f8beb880 100644 --- a/app/tests/test_cbuf.c +++ b/app/tests/test_cbuf.c @@ -65,7 +65,10 @@ static void test_cbuf_push_take(void) { assert(item == 35); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_cbuf_empty(); test_cbuf_full(); test_cbuf_push_take(); diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 07974361..7a7d408d 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -122,7 +122,10 @@ static void test_options2(void) { assert(opts->record_format == SC_RECORD_FORMAT_MP4); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_flag_version(); test_flag_help(); test_options(); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 592c2628..b58c8e20 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -257,7 +257,10 @@ static void test_serialize_rotate_device(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index 8fcfc93d..3dfd0b0f 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -45,7 +45,10 @@ static void test_deserialize_clipboard_big(void) { device_msg_destroy(&msg); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_deserialize_clipboard(); test_deserialize_clipboard_big(); return 0; diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c index b0950bb0..e10821cd 100644 --- a/app/tests/test_queue.c +++ b/app/tests/test_queue.c @@ -32,7 +32,10 @@ static void test_queue(void) { assert(queue_is_empty(&queue)); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_queue(); return 0; } diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index a88bca0e..7b9c61da 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -286,7 +286,10 @@ static void test_parse_integer_with_suffix(void) { assert(!ok); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_xstrncpy_simple(); test_xstrncpy_just_fit(); test_xstrncpy_truncated(); From eabaf6f7bd391d0147eaebda51c79bbf9a75807a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 168/214] Simplify PASTE option for "set clipboard" When the client requests to set the clipboard, it may request to press the PASTE key in addition. To be a bit generic, it was stored as a flag in ControlMessage.java. But flags suggest that it represents a bitwise union. Use a simple boolean instead. --- .../java/com/genymobile/scrcpy/ControlMessage.java | 12 ++++-------- .../com/genymobile/scrcpy/ControlMessageReader.java | 4 ++-- .../main/java/com/genymobile/scrcpy/Controller.java | 3 +-- .../genymobile/scrcpy/ControlMessageReaderTest.java | 8 ++------ 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index dbb8d382..736acf80 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -17,8 +17,6 @@ public final class ControlMessage { public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_ROTATE_DEVICE = 10; - public static final int FLAGS_PASTE = 1; - private int type; private String text; private int metaState; // KeyEvent.META_* @@ -30,7 +28,7 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; - private int flags; + private boolean paste; private int repeat; private ControlMessage() { @@ -77,9 +75,7 @@ public final class ControlMessage { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; msg.text = text; - if (paste) { - msg.flags = FLAGS_PASTE; - } + msg.paste = paste; return msg; } @@ -143,8 +139,8 @@ public final class ControlMessage { return vScroll; } - public int getFlags() { - return flags; + public boolean getPaste() { + return paste; } public int getRepeat() { diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 132e3f4e..ce185103 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -154,12 +154,12 @@ public class ControlMessageReader { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; } - boolean parse = buffer.get() != 0; + boolean paste = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text, parse); + return ControlMessage.createSetClipboard(text, paste); } private ControlMessage parseSetScreenPowerMode() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 6ff8d208..f32e5ec0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -110,8 +110,7 @@ public class Controller { } break; case ControlMessage.TYPE_SET_CLIPBOARD: - boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - setClipboard(msg.getText(), paste); + setClipboard(msg.getText(), msg.getPaste()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 1e2b8266..5eb52760 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -230,9 +230,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals("testé", event.getText()); - - boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - Assert.assertTrue(parse); + Assert.assertTrue(event.getPaste()); } @Test @@ -258,9 +256,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(text, event.getText()); - - boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - Assert.assertTrue(parse); + Assert.assertTrue(event.getPaste()); } @Test From 9d9dd1f143e6dae770263432a11946f1601be82e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 169/214] Make expression order consistent The condition "cmd" was always before "shift" in all expressions except 4. --- app/src/input_manager.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 7d500efe..465ba6e6 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -368,22 +368,22 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_f: - if (!shift && cmd && !repeat && down) { + if (cmd && !shift && !repeat && down) { screen_switch_fullscreen(im->screen); } return; case SDLK_x: - if (!shift && cmd && !repeat && down) { + if (cmd && !shift && !repeat && down) { screen_resize_to_fit(im->screen); } return; case SDLK_g: - if (!shift && cmd && !repeat && down) { + if (cmd && !shift && !repeat && down) { screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: - if (!shift && cmd && !repeat && down) { + if (cmd && !shift && !repeat && down) { struct fps_counter *fps_counter = im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); From 63cb93d7d7d90354b834e71200eb43ee9228f2a3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 170/214] Use Ctrl for all shortcuts Remove the Cmd modifier on macOS, which was possible only for some shortcuts but not all. This paves the way to make the shortcut modifier customizable. --- README.md | 48 ++++++++++++++++----------------- app/src/cli.c | 45 ++++++++++++++----------------- app/src/input_manager.c | 59 +++++++++++++++-------------------------- 3 files changed, 66 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 2c01a01e..3cb9021c 100644 --- a/README.md +++ b/README.md @@ -571,30 +571,30 @@ Also see [issue #14]. ## Shortcuts - | Action | Shortcut | Shortcut (macOS) - | ------------------------------------------- |:----------------------------- |:----------------------------- - | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` - | Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ - | Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ - | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` - | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ - | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ - | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ - | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` - | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` - | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ - | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ - | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` - | Power on | _Right-click²_ | _Right-click²_ - | Turn device screen off (keep mirroring) | `Ctrl`+`o` | `Cmd`+`o` - | Turn device screen on | `Ctrl`+`Shift`+`o` | `Cmd`+`Shift`+`o` - | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` - | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` - | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` - | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` - | Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v` - | Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` - | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` + | Action | Shortcut + | ------------------------------------------- |:----------------------------- + | Switch fullscreen mode | `Ctrl`+`f` + | Rotate display left | `Ctrl`+`←` _(left)_ + | Rotate display right | `Ctrl`+`→` _(right)_ + | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` + | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ + | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ + | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ + | Click on `APP_SWITCH` | `Ctrl`+`s` + | Click on `MENU` | `Ctrl`+`m` + | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ + | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ + | Click on `POWER` | `Ctrl`+`p` + | Power on | _Right-click²_ + | Turn device screen off (keep mirroring) | `Ctrl`+`o` + | Turn device screen on | `Ctrl`+`Shift`+`o` + | Rotate device screen | `Ctrl`+`r` + | Expand notification panel | `Ctrl`+`n` + | Collapse notification panel | `Ctrl`+`Shift`+`n` + | Copy device clipboard to computer | `Ctrl`+`c` + | Paste computer clipboard to device | `Ctrl`+`v` + | Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` + | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ diff --git a/app/src/cli.c b/app/src/cli.c index 9e07e912..4b65c98f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -13,11 +13,6 @@ void scrcpy_print_usage(const char *arg0) { -#ifdef __APPLE__ -# define CTRL_OR_CMD "Cmd" -#else -# define CTRL_OR_CMD "Ctrl" -#endif fprintf(stderr, "Usage: %s [options]\n" "\n" @@ -190,19 +185,19 @@ scrcpy_print_usage(const char *arg0) { "\n" "Shortcuts:\n" "\n" - " " CTRL_OR_CMD "+f\n" + " Ctrl+f\n" " Switch fullscreen mode\n" "\n" - " " CTRL_OR_CMD "+Left\n" + " Ctrl+Left\n" " Rotate display left\n" "\n" - " " CTRL_OR_CMD "+Right\n" + " Ctrl+Right\n" " Rotate display right\n" "\n" - " " CTRL_OR_CMD "+g\n" + " Ctrl+g\n" " Resize window to 1:1 (pixel-perfect)\n" "\n" - " " CTRL_OR_CMD "+x\n" + " Ctrl+x\n" " Double-click on black borders\n" " Resize window to remove black borders\n" "\n" @@ -210,55 +205,55 @@ scrcpy_print_usage(const char *arg0) { " Middle-click\n" " Click on HOME\n" "\n" - " " CTRL_OR_CMD "+b\n" - " " CTRL_OR_CMD "+Backspace\n" + " Ctrl+b\n" + " Ctrl+Backspace\n" " Right-click (when screen is on)\n" " Click on BACK\n" "\n" - " " CTRL_OR_CMD "+s\n" + " Ctrl+s\n" " Click on APP_SWITCH\n" "\n" " Ctrl+m\n" " Click on MENU\n" "\n" - " " CTRL_OR_CMD "+Up\n" + " Ctrl+Up\n" " Click on VOLUME_UP\n" "\n" - " " CTRL_OR_CMD "+Down\n" + " Ctrl+Down\n" " Click on VOLUME_DOWN\n" "\n" - " " CTRL_OR_CMD "+p\n" + " Ctrl+p\n" " Click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" " Power on\n" "\n" - " " CTRL_OR_CMD "+o\n" + " Ctrl+o\n" " Turn device screen off (keep mirroring)\n" "\n" - " " CTRL_OR_CMD "+Shift+o\n" + " Ctrl+Shift+o\n" " Turn device screen on\n" "\n" - " " CTRL_OR_CMD "+r\n" + " Ctrl+r\n" " Rotate device screen\n" "\n" - " " CTRL_OR_CMD "+n\n" + " Ctrl+n\n" " Expand notification panel\n" "\n" - " " CTRL_OR_CMD "+Shift+n\n" + " Ctrl+Shift+n\n" " Collapse notification panel\n" "\n" - " " CTRL_OR_CMD "+c\n" + " Ctrl+c\n" " Copy device clipboard to computer\n" "\n" - " " CTRL_OR_CMD "+v\n" + " Ctrl+v\n" " Paste computer clipboard to device\n" "\n" - " " CTRL_OR_CMD "+Shift+v\n" + " Ctrl+Shift+v\n" " Copy computer clipboard to device (and paste if the device\n" " runs Android >= 7)\n" "\n" - " " CTRL_OR_CMD "+i\n" + " Ctrl+i\n" " Enable/disable FPS counter (print frames/second in logs)\n" "\n" " Drag & drop APK file\n" diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 465ba6e6..e16dee52 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -264,27 +264,16 @@ input_manager_process_key(struct input_manager *im, bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT); bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI); - // use Cmd on macOS, Ctrl on other platforms -#ifdef __APPLE__ - bool cmd = !ctrl && meta; -#else - if (meta) { - // no shortcuts involve Meta on platforms other than macOS, and it must - // not be forwarded to the device - return; - } - bool cmd = ctrl; // && !meta, already guaranteed -#endif - - if (alt) { - // no shortcuts involve Alt, and it must not be forwarded to the device + if (alt || meta) { + // no shortcuts involve Alt or Meta, and they must not be forwarded to + // the device return; } struct controller *controller = im->controller; // capture all Ctrl events - if (ctrl || cmd) { + if (ctrl) { SDL_Keycode keycode = event->keysym.sym; bool down = event->type == SDL_KEYDOWN; int action = down ? ACTION_DOWN : ACTION_UP; @@ -292,37 +281,33 @@ input_manager_process_key(struct input_manager *im, bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); switch (keycode) { case SDLK_h: - // Ctrl+h on all platform, since Cmd+h is already captured by - // the system on macOS to hide the window - if (control && ctrl && !meta && !shift && !repeat) { + if (control && ctrl && !shift && !repeat) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && cmd && !shift && !repeat) { + if (control && ctrl && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: - if (control && cmd && !shift && !repeat) { + if (control && ctrl && !shift && !repeat) { action_app_switch(controller, action); } return; case SDLK_m: - // Ctrl+m on all platform, since Cmd+m is already captured by - // the system on macOS to minimize the window - if (control && ctrl && !meta && !shift && !repeat) { + if (control && ctrl && !shift && !repeat) { action_menu(controller, action); } return; case SDLK_p: - if (control && cmd && !shift && !repeat) { + if (control && ctrl && !shift && !repeat) { action_power(controller, action); } return; case SDLK_o: - if (control && cmd && !repeat && down) { + if (control && ctrl && !repeat && down) { enum screen_power_mode mode = shift ? SCREEN_POWER_MODE_NORMAL : SCREEN_POWER_MODE_OFF; @@ -330,34 +315,34 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_DOWN: - if (control && cmd && !shift) { + if (control && ctrl && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (control && cmd && !shift) { + if (control && ctrl && !shift) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_LEFT: - if (cmd && !shift && !repeat && down) { + if (ctrl && !shift && !repeat && down) { rotate_client_left(im->screen); } return; case SDLK_RIGHT: - if (cmd && !shift && !repeat && down) { + if (ctrl && !shift && !repeat && down) { rotate_client_right(im->screen); } return; case SDLK_c: - if (control && cmd && !shift && !repeat && down) { + if (control && ctrl && !shift && !repeat && down) { request_device_clipboard(controller); } return; case SDLK_v: - if (control && cmd && !repeat && down) { + if (control && ctrl && !repeat && down) { if (shift) { // store the text in the device clipboard and paste set_device_clipboard(controller, true); @@ -368,29 +353,29 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_f: - if (cmd && !shift && !repeat && down) { + if (ctrl && !shift && !repeat && down) { screen_switch_fullscreen(im->screen); } return; case SDLK_x: - if (cmd && !shift && !repeat && down) { + if (ctrl && !shift && !repeat && down) { screen_resize_to_fit(im->screen); } return; case SDLK_g: - if (cmd && !shift && !repeat && down) { + if (ctrl && !shift && !repeat && down) { screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: - if (cmd && !shift && !repeat && down) { + if (ctrl && !shift && !repeat && down) { struct fps_counter *fps_counter = im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; case SDLK_n: - if (control && cmd && !repeat && down) { + if (control && ctrl && !repeat && down) { if (shift) { collapse_notification_panel(controller); } else { @@ -399,7 +384,7 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_r: - if (control && cmd && !shift && !repeat && down) { + if (control && ctrl && !shift && !repeat && down) { rotate_device(controller); } return; From 1b76d9fd78c3a88a8503a72d4cd2f65bdb836aa4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 171/214] Customize shortcut modifier Add --shortcut-mod, and use Alt as default modifier. This paves the way to forward the Ctrl key to the device. --- README.md | 82 ++++++++++++-------- app/meson.build | 2 +- app/scrcpy.1 | 55 ++++++++------ app/src/cli.c | 162 ++++++++++++++++++++++++++++++++++------ app/src/cli.h | 5 ++ app/src/input_manager.c | 110 +++++++++++++++++++-------- app/src/input_manager.h | 14 +++- app/src/scrcpy.c | 11 ++- app/src/scrcpy.h | 21 ++++++ app/tests/test_cli.c | 39 ++++++++++ 10 files changed, 392 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 3cb9021c..7eb5578a 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ scrcpy --fullscreen scrcpy -f # short version ``` -Fullscreen can then be toggled dynamically with `Ctrl`+`f`. +Fullscreen can then be toggled dynamically with `MOD`+`f`. #### Rotation @@ -370,17 +370,17 @@ Possibles values are: - `2`: 180 degrees - `3`: 90 degrees clockwise -The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and -`Ctrl`+`→` _(right)_. +The rotation can also be changed dynamically with `MOD`+`←` _(left)_ and +`MOD`+`→` _(right)_. Note that _scrcpy_ manages 3 different rotations: - - `Ctrl`+`r` requests the device to switch between portrait and landscape (the + - `MOD`+`r` requests the device to switch between portrait and landscape (the current running app may refuse, if it does support the requested orientation). - `--lock-video-orientation` changes the mirroring orientation (the orientation of the video sent from the device to the computer). This affects the recording. - - `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This + - `--rotation` (or `MOD`+`←`/`MOD`+`→`) rotates only the window content. This affects only the display, not the recording. @@ -437,9 +437,9 @@ scrcpy --turn-screen-off scrcpy -S ``` -Or by pressing `Ctrl`+`o` at any time. +Or by pressing `MOD`+`o` at any time. -To turn it back on, press `Ctrl`+`Shift`+`o` (or `POWER`, `Ctrl`+`p`). +To turn it back on, press `MOD`+`Shift`+`o` (or `POWER`, `MOD`+`p`). It can be useful to also prevent the device to sleep: @@ -494,7 +494,7 @@ scrcpy --disable-screensaver #### Rotate device screen -Press `Ctrl`+`r` to switch between portrait and landscape modes. +Press `MOD`+`r` to switch between portrait and landscape modes. Note that it rotates only if the application in foreground supports the requested orientation. @@ -504,10 +504,10 @@ requested orientation. It is possible to synchronize clipboards between the computer and the device, in both directions: - - `Ctrl`+`c` copies the device clipboard to the computer clipboard; - - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard (and + - `MOD`+`c` copies the device clipboard to the computer clipboard; + - `MOD`+`Shift`+`v` copies the computer clipboard to the device clipboard (and pastes if the device runs Android >= 7); - - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but + - `MOD`+`v` _pastes_ the computer clipboard as a sequence of text events (but breaks non-ASCII characters). Moreover, any time the Android clipboard changes, it is automatically @@ -571,30 +571,48 @@ Also see [issue #14]. ## Shortcuts +In the following list, `MOD` is the shortcut modifier. By default, it's (left) +`Alt`. + +It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, +`lalt`, `ralt`, `lsuper` and `rsuper`. For example: + +```bash +# use RCtrl for shortcuts +scrcpy --shortcut-mod=rctrl + +# use either LCtrl+LAlt or LSuper for shortcuts +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] is typically the "Windows" or "Cmd" key._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + | Action | Shortcut | ------------------------------------------- |:----------------------------- - | Switch fullscreen mode | `Ctrl`+`f` - | Rotate display left | `Ctrl`+`←` _(left)_ - | Rotate display right | `Ctrl`+`→` _(right)_ - | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` - | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ - | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ - | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ - | Click on `APP_SWITCH` | `Ctrl`+`s` - | Click on `MENU` | `Ctrl`+`m` - | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ - | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ - | Click on `POWER` | `Ctrl`+`p` + | Switch fullscreen mode | `MOD`+`f` + | Rotate display left | `MOD`+`←` _(left)_ + | Rotate display right | `MOD`+`→` _(right)_ + | Resize window to 1:1 (pixel-perfect) | `MOD`+`g` + | Resize window to remove black borders | `MOD`+`x` \| _Double-click¹_ + | Click on `HOME` | `MOD`+`h` \| _Middle-click_ + | Click on `BACK` | `MOD`+`b` \| _Right-click²_ + | Click on `APP_SWITCH` | `MOD`+`s` + | Click on `MENU` | `MOD`+`m` + | Click on `VOLUME_UP` | `MOD`+`↑` _(up)_ + | Click on `VOLUME_DOWN` | `MOD`+`↓` _(down)_ + | Click on `POWER` | `MOD`+`p` | Power on | _Right-click²_ - | Turn device screen off (keep mirroring) | `Ctrl`+`o` - | Turn device screen on | `Ctrl`+`Shift`+`o` - | Rotate device screen | `Ctrl`+`r` - | Expand notification panel | `Ctrl`+`n` - | Collapse notification panel | `Ctrl`+`Shift`+`n` - | Copy device clipboard to computer | `Ctrl`+`c` - | Paste computer clipboard to device | `Ctrl`+`v` - | Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` - | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` + | Turn device screen off (keep mirroring) | `MOD`+`o` + | Turn device screen on | `MOD`+`Shift`+`o` + | Rotate device screen | `MOD`+`r` + | Expand notification panel | `MOD`+`n` + | Collapse notification panel | `MOD`+`Shift`+`n` + | Copy device clipboard to computer | `MOD`+`c` + | Paste computer clipboard to device | `MOD`+`v` + | Copy computer clipboard to device and paste | `MOD`+`Shift`+`v` + | Enable/disable FPS counter (on stdout) | `MOD`+`i` _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ diff --git a/app/meson.build b/app/meson.build index 505f0819..0163dd7f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -186,7 +186,7 @@ if get_option('buildtype') == 'debug' exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies, - c_args: ['-DSDL_MAIN_HANDLED']) + c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) test(t[0], exe) endforeach endif diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 4e3c43f2..7ea778f3 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -149,6 +149,16 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. +.TP +.BI "\-\-shortcut\-mod " key[+...]][,...] +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 ','. + +For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper". + +Default is "lalt" (left-Alt). + .TP .B \-S, \-\-turn\-screen\-off Turn the device screen off immediately. @@ -207,52 +217,55 @@ Default is 0 (automatic).\n .SH SHORTCUTS +In the following list, MOD is the shortcut modifier. By default, it's (left) +Alt, but it can be configured by \-\-shortcut-mod. + .TP -.B Ctrl+f +.B MOD+f Switch fullscreen mode .TP -.B Ctrl+Left +.B MOD+Left Rotate display left .TP -.B Ctrl+Right +.B MOD+Right Rotate display right .TP -.B Ctrl+g +.B MOD+g Resize window to 1:1 (pixel\-perfect) .TP -.B Ctrl+x, Double\-click on black borders +.B MOD+x, Double\-click on black borders Resize window to remove black borders .TP -.B Ctrl+h, Home, Middle\-click +.B MOD+h, Home, Middle\-click Click on HOME .TP -.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on) +.B MOD+b, MOD+Backspace, Right\-click (when screen is on) Click on BACK .TP -.B Ctrl+s +.B MOD+s Click on APP_SWITCH .TP -.B Ctrl+m +.B MOD+m Click on MENU .TP -.B Ctrl+Up +.B MOD+Up Click on VOLUME_UP .TP -.B Ctrl+Down +.B MOD+Down Click on VOLUME_DOWN .TP -.B Ctrl+p +.B MOD+p Click on POWER (turn screen on/off) .TP @@ -260,39 +273,39 @@ Click on POWER (turn screen on/off) Turn screen on .TP -.B Ctrl+o +.B MOD+o Turn device screen off (keep mirroring) .TP -.B Ctrl+Shift+o +.B MOD+Shift+o Turn device screen on .TP -.B Ctrl+r +.B MOD+r Rotate device screen .TP -.B Ctrl+n +.B MOD+n Expand notification panel .TP -.B Ctrl+Shift+n +.B MOD+Shift+n Collapse notification panel .TP -.B Ctrl+c +.B MOD+c Copy device clipboard to computer .TP -.B Ctrl+v +.B MOD+v Paste computer clipboard to device .TP -.B Ctrl+Shift+v +.B MOD+Shift+v Copy computer clipboard to device (and paste if the device runs Android >= 7) .TP -.B Ctrl+i +.B MOD+i Enable/disable FPS counter (print frames/second in logs) .TP diff --git a/app/src/cli.c b/app/src/cli.c index 4b65c98f..de32810e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -138,6 +138,19 @@ scrcpy_print_usage(const char *arg0) { " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" "\n" + " --shortcut-mod key[+...]][,...]\n" + " Specify the modifiers to use for scrcpy shortcuts.\n" + " Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\",\n" + " \"lsuper\" and \"rsuper\".\n" + "\n" + " A shortcut can consist in several keys, separated by '+'.\n" + " Several shortcuts can be specified, separated by ','.\n" + "\n" + " For example, to use either LCtrl+LAlt or LSuper for scrcpy\n" + " shortcuts, pass \"lctrl+lalt,lsuper\".\n" + "\n" + " Default is \"lalt\" (left-Alt).\n" + "\n" " -S, --turn-screen-off\n" " Turn the device screen off immediately.\n" "\n" @@ -185,75 +198,78 @@ scrcpy_print_usage(const char *arg0) { "\n" "Shortcuts:\n" "\n" - " Ctrl+f\n" + " In the following list, MOD is the shortcut modifier. By default,\n" + " it's (left) Alt, but it can be configured by --shortcut-mod.\n" + "\n" + " MOD+f\n" " Switch fullscreen mode\n" "\n" - " Ctrl+Left\n" + " MOD+Left\n" " Rotate display left\n" "\n" - " Ctrl+Right\n" + " MOD+Right\n" " Rotate display right\n" "\n" - " Ctrl+g\n" + " MOD+g\n" " Resize window to 1:1 (pixel-perfect)\n" "\n" - " Ctrl+x\n" + " MOD+x\n" " Double-click on black borders\n" " Resize window to remove black borders\n" "\n" - " Ctrl+h\n" + " MOD+h\n" " Middle-click\n" " Click on HOME\n" "\n" - " Ctrl+b\n" - " Ctrl+Backspace\n" + " MOD+b\n" + " MOD+Backspace\n" " Right-click (when screen is on)\n" " Click on BACK\n" "\n" - " Ctrl+s\n" + " MOD+s\n" " Click on APP_SWITCH\n" "\n" - " Ctrl+m\n" + " MOD+m\n" " Click on MENU\n" "\n" - " Ctrl+Up\n" + " MOD+Up\n" " Click on VOLUME_UP\n" "\n" - " Ctrl+Down\n" + " MOD+Down\n" " Click on VOLUME_DOWN\n" "\n" - " Ctrl+p\n" + " MOD+p\n" " Click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" " Power on\n" "\n" - " Ctrl+o\n" + " MOD+o\n" " Turn device screen off (keep mirroring)\n" "\n" - " Ctrl+Shift+o\n" + " MOD+Shift+o\n" " Turn device screen on\n" "\n" - " Ctrl+r\n" + " MOD+r\n" " Rotate device screen\n" "\n" - " Ctrl+n\n" + " MOD+n\n" " Expand notification panel\n" "\n" - " Ctrl+Shift+n\n" + " MOD+Shift+n\n" " Collapse notification panel\n" "\n" - " Ctrl+c\n" + " MOD+c\n" " Copy device clipboard to computer\n" "\n" - " Ctrl+v\n" + " MOD+v\n" " Paste computer clipboard to device\n" "\n" - " Ctrl+Shift+v\n" + " MOD+Shift+v\n" " Copy computer clipboard to device (and paste if the device\n" " runs Android >= 7)\n" "\n" - " Ctrl+i\n" + " MOD+i\n" " Enable/disable FPS counter (print frames/second in logs)\n" "\n" " Drag & drop APK file\n" @@ -475,6 +491,101 @@ 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_MOD_* constants (or 0 on error) +static unsigned +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_MOD_LCTRL; + } else if (STREQ("rctrl", item, key_len)) { + mod |= SC_MOD_RCTRL; + } else if (STREQ("lalt", item, key_len)) { + mod |= SC_MOD_LALT; + } else if (STREQ("ralt", item, key_len)) { + mod |= SC_MOD_RALT; + } else if (STREQ("lsuper", item, key_len)) { + mod |= SC_MOD_LSUPER; + } else if (STREQ("rsuper", item, key_len)) { + mod |= SC_MOD_RSUPER; + } else { + LOGW("Unknown modifier key: %.*s", (int) key_len, item); + return 0; + } +#undef STREQ + + if (!has_plus) { + break; + } + + item = plus + 1; + assert(len >= key_len + 1); + len -= key_len + 1; + } + + return mod; +} + +static bool +parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { + unsigned count = 0; + unsigned current = 0; + + // LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+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); + if (!mod) { + LOGE("Invalid modifier keys: %.*s", (int) limit, s); + return false; + } + + mods->data[current++] = mod; + ++count; + + if (!comma) { + break; + } + + s = comma + 1; + } + + mods->count = count; + + return true; +} + +#ifdef SC_TEST +// expose the function to unit-tests +bool +sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { + return parse_shortcut_mods(s, mods); +} +#endif + static bool parse_record_format(const char *optarg, enum sc_record_format *format) { if (!strcmp(optarg, "mp4")) { @@ -526,6 +637,7 @@ guess_record_format(const char *filename) { #define OPT_CODEC_OPTIONS 1018 #define OPT_FORCE_ADB_FORWARD 1019 #define OPT_DISABLE_SCREENSAVER 1020 +#define OPT_SHORTCUT_MOD 1021 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -558,6 +670,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_RENDER_EXPIRED_FRAMES}, {"rotation", required_argument, NULL, OPT_ROTATION}, {"serial", required_argument, NULL, 's'}, + {"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD}, {"show-touches", no_argument, NULL, 't'}, {"stay-awake", no_argument, NULL, 'w'}, {"turn-screen-off", no_argument, NULL, 'S'}, @@ -721,6 +834,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_DISABLE_SCREENSAVER: opts->disable_screensaver = true; break; + case OPT_SHORTCUT_MOD: + if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/cli.h b/app/src/cli.h index 2e2bfe93..73dfe228 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -18,4 +18,9 @@ scrcpy_print_usage(const char *arg0); bool 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); +#endif + #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e16dee52..b954fbd9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,6 +1,7 @@ #include "input_manager.h" #include +#include #include "config.h" #include "event_converter.h" @@ -10,6 +11,64 @@ static const int ACTION_DOWN = 1; static const int ACTION_UP = 1 << 1; +#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) + +static inline uint16_t +to_sdl_mod(unsigned mod) { + uint16_t sdl_mod = 0; + if (mod & SC_MOD_LCTRL) { + sdl_mod |= KMOD_LCTRL; + } + if (mod & SC_MOD_RCTRL) { + sdl_mod |= KMOD_RCTRL; + } + if (mod & SC_MOD_LALT) { + sdl_mod |= KMOD_LALT; + } + if (mod & SC_MOD_RALT) { + sdl_mod |= KMOD_RALT; + } + if (mod & SC_MOD_LSUPER) { + sdl_mod |= KMOD_LGUI; + } + if (mod & SC_MOD_RSUPER) { + sdl_mod |= KMOD_RGUI; + } + return sdl_mod; +} + +static bool +is_shortcut_mod(struct 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; + } + } + + return false; +} + +void +input_manager_init(struct input_manager *im, bool prefer_text, + const struct sc_shortcut_mods *shortcut_mods) +{ + im->prefer_text = prefer_text; + + 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; +} + static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) { @@ -258,22 +317,13 @@ input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, bool control) { // control: indicates the state of the command-line option --no-control - // ctrl: the Ctrl key - bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL); - bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT); - bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI); - - if (alt || meta) { - // no shortcuts involve Alt or Meta, and they must not be forwarded to - // the device - return; - } + bool smod = is_shortcut_mod(im, event->keysym.mod); struct controller *controller = im->controller; - // capture all Ctrl events - if (ctrl) { + // The shortcut modifier is pressed + if (smod) { SDL_Keycode keycode = event->keysym.sym; bool down = event->type == SDL_KEYDOWN; int action = down ? ACTION_DOWN : ACTION_UP; @@ -281,33 +331,33 @@ input_manager_process_key(struct input_manager *im, bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); switch (keycode) { case SDLK_h: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_app_switch(controller, action); } return; case SDLK_m: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_menu(controller, action); } return; case SDLK_p: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_power(controller, action); } return; case SDLK_o: - if (control && ctrl && !repeat && down) { + if (control && !repeat && down) { enum screen_power_mode mode = shift ? SCREEN_POWER_MODE_NORMAL : SCREEN_POWER_MODE_OFF; @@ -315,34 +365,34 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_DOWN: - if (control && ctrl && !shift) { + if (control && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (control && ctrl && !shift) { + if (control && !shift) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_LEFT: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { rotate_client_left(im->screen); } return; case SDLK_RIGHT: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { rotate_client_right(im->screen); } return; case SDLK_c: - if (control && ctrl && !shift && !repeat && down) { + if (control && !shift && !repeat && down) { request_device_clipboard(controller); } return; case SDLK_v: - if (control && ctrl && !repeat && down) { + if (control && !repeat && down) { if (shift) { // store the text in the device clipboard and paste set_device_clipboard(controller, true); @@ -353,29 +403,29 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_f: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { screen_switch_fullscreen(im->screen); } return; case SDLK_x: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { screen_resize_to_fit(im->screen); } return; case SDLK_g: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { struct fps_counter *fps_counter = im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; case SDLK_n: - if (control && ctrl && !repeat && down) { + if (control && !repeat && down) { if (shift) { collapse_notification_panel(controller); } else { @@ -384,7 +434,7 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_r: - if (control && ctrl && !shift && !repeat && down) { + if (control && !shift && !repeat && down) { rotate_device(controller); } return; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 90b74fec..c854664a 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -3,12 +3,15 @@ #include +#include + #include "config.h" #include "common.h" #include "controller.h" #include "fps_counter.h" -#include "video_buffer.h" +#include "scrcpy.h" #include "screen.h" +#include "video_buffer.h" struct input_manager { struct controller *controller; @@ -20,8 +23,17 @@ struct input_manager { unsigned repeat; bool prefer_text; + + struct { + unsigned data[SC_MAX_SHORTCUT_MODS]; + unsigned count; + } sdl_shortcut_mods; }; +void +input_manager_init(struct input_manager *im, bool prefer_text, + const struct sc_shortcut_mods *shortcut_mods); + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 889cbb87..06a3b3d1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -49,7 +49,13 @@ static struct input_manager input_manager = { .video_buffer = &video_buffer, .screen = &screen, .repeat = 0, - .prefer_text = false, // initialized later + + // initialized later + .prefer_text = false, + .sdl_shortcut_mods = { + .data = {0}, + .count = 0, + }, }; #ifdef _WIN32 @@ -437,7 +443,8 @@ scrcpy(const struct scrcpy_options *options) { } } - input_manager.prefer_text = options->prefer_text; + input_manager_init(&input_manager, options->prefer_text, + &options->shortcut_mods); ret = event_loop(options->display, options->control); LOGD("quit..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d7eb2f58..220fcc2c 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -20,6 +20,22 @@ enum sc_record_format { SC_RECORD_FORMAT_MKV, }; +#define SC_MAX_SHORTCUT_MODS 8 + +enum sc_shortcut_mod { + SC_MOD_LCTRL = 1 << 0, + SC_MOD_RCTRL = 1 << 1, + SC_MOD_LALT = 1 << 2, + SC_MOD_RALT = 1 << 3, + SC_MOD_LSUPER = 1 << 4, + SC_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; @@ -38,6 +54,7 @@ struct scrcpy_options { enum sc_log_level log_level; enum sc_record_format record_format; struct sc_port_range port_range; + struct sc_shortcut_mods shortcut_mods; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; @@ -77,6 +94,10 @@ struct scrcpy_options { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ }, \ + .shortcut_mods = { \ + .data = {SC_MOD_LALT}, \ + .count = 1, \ + }, \ .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 7a7d408d..1024dba6 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -3,6 +3,7 @@ #include "cli.h" #include "common.h" +#include "scrcpy.h" static void test_flag_version(void) { struct scrcpy_cli_args args = { @@ -122,6 +123,43 @@ static void test_options2(void) { assert(opts->record_format == SC_RECORD_FORMAT_MP4); } +static void test_parse_shortcut_mods(void) { + struct sc_shortcut_mods mods; + bool ok; + + ok = sc_parse_shortcut_mods("lctrl", &mods); + assert(ok); + assert(mods.count == 1); + assert(mods.data[0] == SC_MOD_LCTRL); + + ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); + assert(ok); + assert(mods.count == 1); + assert(mods.data[0] == (SC_MOD_LCTRL | SC_MOD_LALT)); + + ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); + assert(ok); + assert(mods.count == 2); + assert(mods.data[0] == SC_MOD_RCTRL); + assert(mods.data[1] == SC_MOD_LALT); + + ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods); + assert(ok); + assert(mods.count == 3); + assert(mods.data[0] == SC_MOD_LSUPER); + assert(mods.data[1] == (SC_MOD_RSUPER | SC_MOD_LALT)); + assert(mods.data[2] == (SC_MOD_LCTRL | SC_MOD_RCTRL | SC_MOD_RALT)); + + ok = sc_parse_shortcut_mods("", &mods); + assert(!ok); + + ok = sc_parse_shortcut_mods("lctrl+", &mods); + assert(!ok); + + ok = sc_parse_shortcut_mods("lctrl,", &mods); + assert(!ok); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -130,5 +168,6 @@ int main(int argc, char *argv[]) { test_flag_help(); test_options(); test_options2(); + test_parse_shortcut_mods(); return 0; }; From e4bb7c1d1feb51e3277db086207833611fc20603 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 172/214] Accept Super as shortcut modifier In addition to (left) Alt, also accept (left) Super. This is especially convenient for macOS users (Super is the Cmd key). --- README.md | 2 +- app/scrcpy.1 | 4 ++-- app/src/cli.c | 5 +++-- app/src/scrcpy.h | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7eb5578a..3aa9180c 100644 --- a/README.md +++ b/README.md @@ -572,7 +572,7 @@ Also see [issue #14]. ## Shortcuts In the following list, `MOD` is the shortcut modifier. By default, it's (left) -`Alt`. +`Alt` or (left) `Super`. It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, `lalt`, `ralt`, `lsuper` and `rsuper`. For example: diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7ea778f3..d79b3964 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -157,7 +157,7 @@ A shortcut can consist in several keys, separated by '+'. Several shortcuts can For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper". -Default is "lalt" (left-Alt). +Default is "lalt,lsuper" (left-Alt or left-Super). .TP .B \-S, \-\-turn\-screen\-off @@ -218,7 +218,7 @@ Default is 0 (automatic).\n .SH SHORTCUTS In the following list, MOD is the shortcut modifier. By default, it's (left) -Alt, but it can be configured by \-\-shortcut-mod. +Alt or (left) Super, but it can be configured by \-\-shortcut-mod. .TP .B MOD+f diff --git a/app/src/cli.c b/app/src/cli.c index de32810e..216763a2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -149,7 +149,7 @@ scrcpy_print_usage(const char *arg0) { " For example, to use either LCtrl+LAlt or LSuper for scrcpy\n" " shortcuts, pass \"lctrl+lalt,lsuper\".\n" "\n" - " Default is \"lalt\" (left-Alt).\n" + " Default is \"lalt,lsuper\" (left-Alt or left-Super).\n" "\n" " -S, --turn-screen-off\n" " Turn the device screen off immediately.\n" @@ -199,7 +199,8 @@ scrcpy_print_usage(const char *arg0) { "Shortcuts:\n" "\n" " In the following list, MOD is the shortcut modifier. By default,\n" - " it's (left) Alt, but it can be configured by --shortcut-mod.\n" + " it's (left) Alt or (left) Super, but it can be configured by\n" + " --shortcut-mod.\n" "\n" " MOD+f\n" " Switch fullscreen mode\n" diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 220fcc2c..05af002f 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -95,8 +95,8 @@ struct scrcpy_options { .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ }, \ .shortcut_mods = { \ - .data = {SC_MOD_LALT}, \ - .count = 1, \ + .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ + .count = 2, \ }, \ .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ From a5f8b577c50e9498c43f7d4d9a25287673244c0f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 01:23:08 +0200 Subject: [PATCH 173/214] Ignore text events for shortcuts Pressing Alt+c generates a text event containing "c", so "c" was sent to the device when --prefer-text was enabled. Ignore text events when the mod state matches a shortcut modifier. --- app/src/input_manager.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b954fbd9..78cf19a7 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -269,6 +269,10 @@ rotate_client_right(struct screen *screen) { void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { + if (is_shortcut_mod(im, SDL_GetModState())) { + // A shortcut must never generate text events + return; + } if (!im->prefer_text) { char c = event->text[0]; if (isalpha(c) || c == ' ') { From e6e528f228f762336ad6b37679c592cf46f6c4cf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 174/214] Forward Ctrl to the device Now that the scrcpy shortcut modifier is Alt by default (and can be configured), forward Ctrl to the device. This allows to trigger Android shortcuts. Fixes #555 --- app/src/event_converter.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 1054dcf9..fb835fa3 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -92,6 +92,8 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT); MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_UP, AKEYCODE_DPAD_UP); + MAP(SDLK_LCTRL, AKEYCODE_CTRL_LEFT); + MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT); } if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { @@ -111,8 +113,8 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, } } - if (prefer_text) { - // do not forward alpha and space key events + if (prefer_text && !(mod & KMOD_CTRL)) { + // do not forward alpha and space key events (unless Ctrl is pressed) return false; } From d4ca85d6a83ee7bdb3dce648b9739374ae770662 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 175/214] Forward Shift to the device This allows to select text using Shift+(arrow keys). Fixes #942 --- app/src/event_converter.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index fb835fa3..ab48898d 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -94,6 +94,8 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_UP, AKEYCODE_DPAD_UP); MAP(SDLK_LCTRL, AKEYCODE_CTRL_LEFT); MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT); + MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT); + MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT); } if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { From 7683be8159c7372c7def0b868ba76a426fa7426e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 176/214] Synchronize clipboard on Ctrl+v Pressing Ctrl+v on the device will typically paste the clipboard content. Before sending the key event, synchronize the computer clipboard to the device clipboard to allow seamless copy-paste. --- app/src/input_manager.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 78cf19a7..625f79a7 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -326,13 +326,15 @@ input_manager_process_key(struct input_manager *im, struct controller *controller = im->controller; + SDL_Keycode keycode = event->keysym.sym; + bool down = event->type == SDL_KEYDOWN; + bool ctrl = event->keysym.mod & KMOD_CTRL; + bool shift = event->keysym.mod & KMOD_SHIFT; + bool repeat = event->repeat; + // The shortcut modifier is pressed if (smod) { - SDL_Keycode keycode = event->keysym.sym; - bool down = event->type == SDL_KEYDOWN; int action = down ? ACTION_DOWN : ACTION_UP; - bool repeat = event->repeat; - bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); switch (keycode) { case SDLK_h: if (control && !shift && !repeat) { @@ -457,6 +459,12 @@ input_manager_process_key(struct input_manager *im, im->repeat = 0; } + if (ctrl && !shift && keycode == SDLK_v && down && !repeat) { + // Synchronize the computer clipboard to the device clipboard before + // sending Ctrl+v, to allow seamless copy-paste. + set_device_clipboard(controller, false); + } + struct control_msg msg; if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) { if (!controller_push_msg(controller, &msg)) { From 1223a72eb83ebafb878a25356250896c45b5cefa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 177/214] Set device clipboard only if necessary Do not explicitly set the clipboard text if it already contains the expected content. This avoids possible copy-paste loops between the computer and the device. --- server/src/main/java/com/genymobile/scrcpy/Device.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 375c9ed6..de551f35 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -221,6 +221,16 @@ public final class Device { if (clipboardManager == null) { return false; } + + String currentClipboard = getClipboardText(); + if (currentClipboard == null || currentClipboard.equals(text)) { + // The clipboard already contains the requested text. + // Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause + // the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this + // problem, do not explicitly set the clipboard text if it already contains the expected content. + return false; + } + isSettingClipboard.set(true); boolean ok = clipboardManager.setText(text); isSettingClipboard.set(false); From 20d39250997635ef882df4e2c11144e5f9f82448 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 178/214] Set computer clipboard only if necessary Do not explicitly set the clipboard text if it already contains the expected content. Even if copy-paste loops are avoided by the previous commit, this avoids to trigger a clipboard change on the computer-side. Refs #1580 --- app/src/receiver.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 5ecff3b7..307eb5d5 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -25,10 +25,19 @@ receiver_destroy(struct receiver *receiver) { static void process_msg(struct device_msg *msg) { switch (msg->type) { - case DEVICE_MSG_TYPE_CLIPBOARD: + case DEVICE_MSG_TYPE_CLIPBOARD: { + char *current = SDL_GetClipboardText(); + bool same = current && !strcmp(current, msg->clipboard.text); + SDL_free(current); + if (same) { + LOGD("Computer clipboard unchanged"); + return; + } + LOGI("Device clipboard copied"); SDL_SetClipboardText(msg->clipboard.text); break; + } } } From bccd12bf5c268bdf096ef81971f13703c97bf474 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 179/214] Remove "get clipboard" call Now that every device clipboard change is automatically synchronized to the computer, the "get clipboard" request (bound to MOD+c) is useless. --- README.md | 2 -- app/scrcpy.1 | 4 ---- app/src/cli.c | 3 --- app/src/input_manager.c | 15 --------------- 4 files changed, 24 deletions(-) diff --git a/README.md b/README.md index 3aa9180c..01c5df71 100644 --- a/README.md +++ b/README.md @@ -504,7 +504,6 @@ requested orientation. It is possible to synchronize clipboards between the computer and the device, in both directions: - - `MOD`+`c` copies the device clipboard to the computer clipboard; - `MOD`+`Shift`+`v` copies the computer clipboard to the device clipboard (and pastes if the device runs Android >= 7); - `MOD`+`v` _pastes_ the computer clipboard as a sequence of text events (but @@ -609,7 +608,6 @@ _[Super] is typically the "Windows" or "Cmd" key._ | Rotate device screen | `MOD`+`r` | Expand notification panel | `MOD`+`n` | Collapse notification panel | `MOD`+`Shift`+`n` - | Copy device clipboard to computer | `MOD`+`c` | Paste computer clipboard to device | `MOD`+`v` | Copy computer clipboard to device and paste | `MOD`+`Shift`+`v` | Enable/disable FPS counter (on stdout) | `MOD`+`i` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d79b3964..0353f59b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -292,10 +292,6 @@ Expand notification panel .B MOD+Shift+n Collapse notification panel -.TP -.B MOD+c -Copy device clipboard to computer - .TP .B MOD+v Paste computer clipboard to device diff --git a/app/src/cli.c b/app/src/cli.c index 216763a2..95525401 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -260,9 +260,6 @@ scrcpy_print_usage(const char *arg0) { " MOD+Shift+n\n" " Collapse notification panel\n" "\n" - " MOD+c\n" - " Copy device clipboard to computer\n" - "\n" " MOD+v\n" " Paste computer clipboard to device\n" "\n" diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 625f79a7..099906cb 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -160,16 +160,6 @@ collapse_notification_panel(struct controller *controller) { } } -static void -request_device_clipboard(struct controller *controller) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; - - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request device clipboard"); - } -} - static void set_device_clipboard(struct controller *controller, bool paste) { char *text = SDL_GetClipboardText(); @@ -392,11 +382,6 @@ input_manager_process_key(struct input_manager *im, rotate_client_right(im->screen); } return; - case SDLK_c: - if (control && !shift && !repeat && down) { - request_device_clipboard(controller); - } - return; case SDLK_v: if (control && !repeat && down) { if (shift) { From 8f64a5984b35af115aeb106ac7cb3731f83472ad Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 180/214] Change "resize to fit" shortcut to MOD+w For convenience, MOD+x will be used for injecting the CUT keycode. --- README.md | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 2 +- app/src/input_manager.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 01c5df71..5bfd32d4 100644 --- a/README.md +++ b/README.md @@ -594,7 +594,7 @@ _[Super] is typically the "Windows" or "Cmd" key._ | Rotate display left | `MOD`+`←` _(left)_ | Rotate display right | `MOD`+`→` _(right)_ | Resize window to 1:1 (pixel-perfect) | `MOD`+`g` - | Resize window to remove black borders | `MOD`+`x` \| _Double-click¹_ + | Resize window to remove black borders | `MOD`+`w` \| _Double-click¹_ | Click on `HOME` | `MOD`+`h` \| _Middle-click_ | Click on `BACK` | `MOD`+`b` \| _Right-click²_ | Click on `APP_SWITCH` | `MOD`+`s` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 0353f59b..ee32c8fb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -237,7 +237,7 @@ Rotate display right Resize window to 1:1 (pixel\-perfect) .TP -.B MOD+x, Double\-click on black borders +.B MOD+w, Double\-click on black borders Resize window to remove black borders .TP diff --git a/app/src/cli.c b/app/src/cli.c index 95525401..0662a552 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -214,7 +214,7 @@ scrcpy_print_usage(const char *arg0) { " MOD+g\n" " Resize window to 1:1 (pixel-perfect)\n" "\n" - " MOD+x\n" + " MOD+w\n" " Double-click on black borders\n" " Resize window to remove black borders\n" "\n" diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 099906cb..580f1a67 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -398,7 +398,7 @@ input_manager_process_key(struct input_manager *im, screen_switch_fullscreen(im->screen); } return; - case SDLK_x: + case SDLK_w: if (!shift && !repeat && down) { screen_resize_to_fit(im->screen); } From 56a115b5c5971b7035609d5e0e3eabf97e91f874 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 181/214] Add shortcuts for COPY and CUT Send COPY and CUT on MOD+c and MOD+x (only supported for Android >= 7). The shortcuts Ctrl+c and Ctrl+x should generally also work (even before Android 7), but the active Android app may use them for other actions instead. --- README.md | 5 ++++- app/scrcpy.1 | 8 ++++++++ app/src/cli.c | 6 ++++++ app/src/input_manager.c | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5bfd32d4..96d3926b 100644 --- a/README.md +++ b/README.md @@ -608,12 +608,15 @@ _[Super] is typically the "Windows" or "Cmd" key._ | Rotate device screen | `MOD`+`r` | Expand notification panel | `MOD`+`n` | Collapse notification panel | `MOD`+`Shift`+`n` + | Copy to clipboard³ | `MOD`+`c` + | Cut to clipboard³ | `MOD`+`x` | Paste computer clipboard to device | `MOD`+`v` | Copy computer clipboard to device and paste | `MOD`+`Shift`+`v` | Enable/disable FPS counter (on stdout) | `MOD`+`i` _¹Double-click on black borders to remove them._ -_²Right-click turns the screen on if it was off, presses BACK otherwise._ +_²Right-click turns the screen on if it was off, presses BACK otherwise._ +_³Only on Android >= 7._ ## Custom paths diff --git a/app/scrcpy.1 b/app/scrcpy.1 index ee32c8fb..9193171e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -292,6 +292,14 @@ Expand notification panel .B MOD+Shift+n Collapse notification panel +.TP +.B Mod+c +Copy to clipboard (inject COPY keycode, Android >= 7 only) + +.TP +.B Mod+x +Cut to clipboard (inject CUT keycode, Android >= 7 only) + .TP .B MOD+v Paste computer clipboard to device diff --git a/app/src/cli.c b/app/src/cli.c index 0662a552..ff810aed 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -260,6 +260,12 @@ scrcpy_print_usage(const char *arg0) { " MOD+Shift+n\n" " Collapse notification panel\n" "\n" + " MOD+c\n" + " Copy to clipboard (inject COPY keycode, Android >= 7 only)\n" + "\n" + " MOD+x\n" + " Cut to clipboard (inject CUT keycode, Android >= 7 only)\n" + "\n" " MOD+v\n" " Paste computer clipboard to device\n" "\n" diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 580f1a67..75808f75 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -129,6 +129,16 @@ action_menu(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); } +static inline void +action_copy(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_COPY, actions, "COPY"); +} + +static inline void +action_cut(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_CUT, actions, "CUT"); +} + // turn the screen on if it was off, press BACK otherwise static void press_back_or_turn_screen_on(struct controller *controller) { @@ -382,6 +392,16 @@ input_manager_process_key(struct input_manager *im, rotate_client_right(im->screen); } return; + case SDLK_c: + if (control && !shift && !repeat) { + action_copy(controller, action); + } + return; + case SDLK_x: + if (control && !shift && !repeat) { + action_cut(controller, action); + } + return; case SDLK_v: if (control && !repeat && down) { if (shift) { From 7ad47dfaab57a7e6c7462d7e6a2798a5f122ebaf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 182/214] Swap paste shortcuts For consistency with MOD+c and MOD+x, use MOD+v to inject PASTE. Use Mod+Shift+v to inject clipboard content as a sequence of text events. --- README.md | 4 ++-- app/scrcpy.1 | 4 ++-- app/src/cli.c | 6 +++--- app/src/input_manager.c | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 96d3926b..c20be508 100644 --- a/README.md +++ b/README.md @@ -610,8 +610,8 @@ _[Super] is typically the "Windows" or "Cmd" key._ | Collapse notification panel | `MOD`+`Shift`+`n` | Copy to clipboard³ | `MOD`+`c` | Cut to clipboard³ | `MOD`+`x` - | Paste computer clipboard to device | `MOD`+`v` - | Copy computer clipboard to device and paste | `MOD`+`Shift`+`v` + | Synchronize clipboards and paste³ | `MOD`+`v` + | Inject computer clipboard text | `MOD`+`Shift`+`v` | Enable/disable FPS counter (on stdout) | `MOD`+`i` _¹Double-click on black borders to remove them._ diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9193171e..728d14fe 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -302,11 +302,11 @@ Cut to clipboard (inject CUT keycode, Android >= 7 only) .TP .B MOD+v -Paste computer clipboard to device +Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only) .TP .B MOD+Shift+v -Copy computer clipboard to device (and paste if the device runs Android >= 7) +Inject computer clipboard text as a sequence of key events .TP .B MOD+i diff --git a/app/src/cli.c b/app/src/cli.c index ff810aed..b75ab41a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -267,11 +267,11 @@ scrcpy_print_usage(const char *arg0) { " Cut to clipboard (inject CUT keycode, Android >= 7 only)\n" "\n" " MOD+v\n" - " Paste computer clipboard to device\n" + " Copy computer clipboard to device, then paste (inject PASTE\n" + " keycode, Android >= 7 only)\n" "\n" " MOD+Shift+v\n" - " Copy computer clipboard to device (and paste if the device\n" - " runs Android >= 7)\n" + " Inject computer clipboard text as a sequence of key events\n" "\n" " MOD+i\n" " Enable/disable FPS counter (print frames/second in logs)\n" diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 75808f75..ec1d5a4e 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -405,11 +405,11 @@ input_manager_process_key(struct input_manager *im, case SDLK_v: if (control && !repeat && down) { if (shift) { - // store the text in the device clipboard and paste - set_device_clipboard(controller, true); - } else { // inject the text as input events clipboard_paste(controller); + } else { + // store the text in the device clipboard and paste + set_device_clipboard(controller, true); } } return; From d8b3ba170cb005e67be7dc866aa91134c4101edd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 183/214] Update copy-paste section in README Update documentation regarding the recent copy-paste changes. --- README.md | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c20be508..0a8df973 100644 --- a/README.md +++ b/README.md @@ -501,16 +501,35 @@ requested orientation. #### Copy-paste -It is possible to synchronize clipboards between the computer and the device, in -both directions: +Any time the Android clipboard changes, it is automatically synchronized to the +computer clipboard. - - `MOD`+`Shift`+`v` copies the computer clipboard to the device clipboard (and - pastes if the device runs Android >= 7); - - `MOD`+`v` _pastes_ the computer clipboard as a sequence of text events (but - breaks non-ASCII characters). +Any `Ctrl` shortcut is forwarded to the device. In particular: + - `Ctrl`+`c` typically copies + - `Ctrl`+`x` typically cuts + - `Ctrl`+`v` typically pastes (after computer-to-device clipboard + synchronization) -Moreover, any time the Android clipboard changes, it is automatically -synchronized to the computer clipboard. +This typically works as you expect. + +The actual behavior depends on the active application though. For example, +_Termux_ sends SIGINT on `Ctrl`+`c` instead, and _K-9 Mail_ composes a new +message. + +To copy, cut and paste in such cases (but only supported on Android >= 7): + - `MOD`+`c` injects `COPY` + - `MOD`+`x` injects `CUT` + - `MOD`+`v` injects `PASTE` (after computer-to-device clipboard + synchronization) + +In addition, `MOD`+`Shift`+`v` allows to inject the computer clipboard text as a +sequence of key events. This is useful when the component does not accept text +pasting (for example in _Termux_), but it can break non-ASCII content. + +**WARNING:** Pasting the computer clipboard to the device (either via `Ctrl`+`v` +or `MOD`+`v`) copies the content into the device clipboard. As a consequence, +any Android application could read its content. You should avoid to paste +sensitive content (like passwords) that way. #### Text injection preference From dfb7324d7b06e649c8355164a7d97bd592ef5af8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jul 2020 00:00:42 +0200 Subject: [PATCH 184/214] Mention in README that Ctrl is forwarded --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0a8df973..6df68fde 100644 --- a/README.md +++ b/README.md @@ -637,6 +637,9 @@ _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ _³Only on Android >= 7._ +All `Ctrl`+_key_ shortcuts are forwarded to the device, so they are handled by +the active application. + ## Custom paths From d49cffb93890982ba837e7b7bc4d9ee9405443d6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Aug 2020 16:45:25 +0200 Subject: [PATCH 185/214] Use HTML tag for keys It's prettyier in a browser. --- README.md | 117 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 6df68fde..e1623a36 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ scrcpy --fullscreen scrcpy -f # short version ``` -Fullscreen can then be toggled dynamically with `MOD`+`f`. +Fullscreen can then be toggled dynamically with MOD+f. #### Rotation @@ -370,18 +370,19 @@ Possibles values are: - `2`: 180 degrees - `3`: 90 degrees clockwise -The rotation can also be changed dynamically with `MOD`+`←` _(left)_ and -`MOD`+`→` _(right)_. +The rotation can also be changed dynamically with MOD+ +_(left)_ and MOD+ _(right)_. Note that _scrcpy_ manages 3 different rotations: - - `MOD`+`r` requests the device to switch between portrait and landscape (the - current running app may refuse, if it does support the requested - orientation). +- MOD+r requests the device to switch between portrait and + landscape (the current running app may refuse, if it does support the + requested orientation). - `--lock-video-orientation` changes the mirroring orientation (the orientation of the video sent from the device to the computer). This affects the recording. - - `--rotation` (or `MOD`+`←`/`MOD`+`→`) rotates only the window content. This - affects only the display, not the recording. + - `--rotation` (or MOD+/MOD+) + rotates only the window content. This affects only the display, not the + recording. ### Other mirroring options @@ -437,9 +438,10 @@ scrcpy --turn-screen-off scrcpy -S ``` -Or by pressing `MOD`+`o` at any time. +Or by pressing MOD+o at any time. -To turn it back on, press `MOD`+`Shift`+`o` (or `POWER`, `MOD`+`p`). +To turn it back on, press MOD+Shift+o (or +`POWER`, MOD+p). It can be useful to also prevent the device to sleep: @@ -494,7 +496,8 @@ scrcpy --disable-screensaver #### Rotate device screen -Press `MOD`+`r` to switch between portrait and landscape modes. +Press MOD+r to switch between portrait and landscape +modes. Note that it rotates only if the application in foreground supports the requested orientation. @@ -504,32 +507,34 @@ requested orientation. Any time the Android clipboard changes, it is automatically synchronized to the computer clipboard. -Any `Ctrl` shortcut is forwarded to the device. In particular: - - `Ctrl`+`c` typically copies - - `Ctrl`+`x` typically cuts - - `Ctrl`+`v` typically pastes (after computer-to-device clipboard - synchronization) +Any Ctrl shortcut is forwarded to the device. In particular: + - Ctrl+c typically copies + - Ctrl+x typically cuts + - Ctrl+v typically pastes (after computer-to-device + clipboard synchronization) This typically works as you expect. The actual behavior depends on the active application though. For example, -_Termux_ sends SIGINT on `Ctrl`+`c` instead, and _K-9 Mail_ composes a new -message. +_Termux_ sends SIGINT on Ctrl+c instead, and _K-9 Mail_ +composes a new message. To copy, cut and paste in such cases (but only supported on Android >= 7): - - `MOD`+`c` injects `COPY` - - `MOD`+`x` injects `CUT` - - `MOD`+`v` injects `PASTE` (after computer-to-device clipboard - synchronization) + - MOD+c injects `COPY` + - MOD+x injects `CUT` + - MOD+v injects `PASTE` (after computer-to-device + clipboard synchronization) -In addition, `MOD`+`Shift`+`v` allows to inject the computer clipboard text as a -sequence of key events. This is useful when the component does not accept text -pasting (for example in _Termux_), but it can break non-ASCII content. +In addition, MOD+Shift+v allows to inject the +computer clipboard text as a sequence of key events. This is useful when the +component does not accept text pasting (for example in _Termux_), but it can +break non-ASCII content. -**WARNING:** Pasting the computer clipboard to the device (either via `Ctrl`+`v` -or `MOD`+`v`) copies the content into the device clipboard. As a consequence, -any Android application could read its content. You should avoid to paste -sensitive content (like passwords) that way. +**WARNING:** Pasting the computer clipboard to the device (either via +Ctrl+v or MOD+v) copies the content +into the device clipboard. As a consequence, any Android application could read +its content. You should avoid to paste sensitive content (like passwords) that +way. #### Text injection preference @@ -589,8 +594,8 @@ Also see [issue #14]. ## Shortcuts -In the following list, `MOD` is the shortcut modifier. By default, it's (left) -`Alt` or (left) `Super`. +In the following list, MOD is the shortcut modifier. By default, it's +(left) Alt or (left) Super. It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, `lalt`, `ralt`, `lsuper` and `rsuper`. For example: @@ -603,42 +608,42 @@ scrcpy --shortcut-mod=rctrl scrcpy --shortcut-mod=lctrl+lalt,lsuper ``` -_[Super] is typically the "Windows" or "Cmd" key._ +_[Super] is typically the Windows or Cmd key._ [Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) | Action | Shortcut | ------------------------------------------- |:----------------------------- - | Switch fullscreen mode | `MOD`+`f` - | Rotate display left | `MOD`+`←` _(left)_ - | Rotate display right | `MOD`+`→` _(right)_ - | Resize window to 1:1 (pixel-perfect) | `MOD`+`g` - | Resize window to remove black borders | `MOD`+`w` \| _Double-click¹_ - | Click on `HOME` | `MOD`+`h` \| _Middle-click_ - | Click on `BACK` | `MOD`+`b` \| _Right-click²_ - | Click on `APP_SWITCH` | `MOD`+`s` - | Click on `MENU` | `MOD`+`m` - | Click on `VOLUME_UP` | `MOD`+`↑` _(up)_ - | Click on `VOLUME_DOWN` | `MOD`+`↓` _(down)_ - | Click on `POWER` | `MOD`+`p` + | Switch fullscreen mode | MOD+f + | Rotate display left | MOD+ _(left)_ + | Rotate display right | MOD+ _(right)_ + | Resize window to 1:1 (pixel-perfect) | MOD+g + | Resize window to remove black borders | MOD+w \| _Double-click¹_ + | Click on `HOME` | MOD+h \| _Middle-click_ + | Click on `BACK` | MOD+b \| _Right-click²_ + | Click on `APP_SWITCH` | MOD+s + | Click on `MENU` | MOD+m + | Click on `VOLUME_UP` | MOD+ _(up)_ + | Click on `VOLUME_DOWN` | MOD+ _(down)_ + | Click on `POWER` | MOD+p | Power on | _Right-click²_ - | Turn device screen off (keep mirroring) | `MOD`+`o` - | Turn device screen on | `MOD`+`Shift`+`o` - | Rotate device screen | `MOD`+`r` - | Expand notification panel | `MOD`+`n` - | Collapse notification panel | `MOD`+`Shift`+`n` - | Copy to clipboard³ | `MOD`+`c` - | Cut to clipboard³ | `MOD`+`x` - | Synchronize clipboards and paste³ | `MOD`+`v` - | Inject computer clipboard text | `MOD`+`Shift`+`v` - | Enable/disable FPS counter (on stdout) | `MOD`+`i` + | Turn device screen off (keep mirroring) | MOD+o + | Turn device screen on | MOD+Shift+o + | Rotate device screen | MOD+r + | Expand notification panel | MOD+n + | Collapse notification panel | MOD+Shift+n + | Copy to clipboard³ | MOD+c + | Cut to clipboard³ | MOD+x + | Synchronize clipboards and paste³ | MOD+v + | Inject computer clipboard text | MOD+Shift+v + | Enable/disable FPS counter (on stdout) | MOD+i _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ _³Only on Android >= 7._ -All `Ctrl`+_key_ shortcuts are forwarded to the device, so they are handled by -the active application. +All Ctrl+_key_ shortcuts are forwarded to the device, so they are +handled by the active application. ## Custom paths From 0870d8620f327768249214a5ba6a7bee7a8a75ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Aug 2020 17:10:09 +0200 Subject: [PATCH 186/214] Mention that MENU unlocks screen Pressing MENU while in lock screen unlocks (it still asks for the schema or code if enabled). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1623a36..b3a5c6b6 100644 --- a/README.md +++ b/README.md @@ -622,7 +622,7 @@ _[Super] is typically the Windows or Cmd key._ | Click on `HOME` | MOD+h \| _Middle-click_ | Click on `BACK` | MOD+b \| _Right-click²_ | Click on `APP_SWITCH` | MOD+s - | Click on `MENU` | MOD+m + | Click on `MENU` (unlock screen) | MOD+m | Click on `VOLUME_UP` | MOD+ _(up)_ | Click on `VOLUME_DOWN` | MOD+ _(down)_ | Click on `POWER` | MOD+p From 74079ea5e4ce0282a5d078cfa80fc68d2d9da16e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 2 Aug 2020 15:45:31 +0200 Subject: [PATCH 187/214] Copy the options used in input manager init This avoids to pass additional options to some input manager functions. Refs #1623 --- app/src/input_manager.c | 17 ++++++++++------- app/src/input_manager.h | 11 +++++------ app/src/scrcpy.c | 8 +++----- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index ec1d5a4e..3bf2282e 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -54,11 +54,13 @@ is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { } void -input_manager_init(struct input_manager *im, bool prefer_text, - const struct sc_shortcut_mods *shortcut_mods) +input_manager_init(struct input_manager *im, + const struct scrcpy_options *options) { - im->prefer_text = prefer_text; + im->control = options->control; + im->prefer_text = options->prefer_text; + const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; assert(shortcut_mods->count); assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); for (unsigned i = 0; i < shortcut_mods->count; ++i) { @@ -318,9 +320,9 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, void input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event, - bool control) { + const SDL_KeyboardEvent *event) { // control: indicates the state of the command-line option --no-control + bool control = im->control; bool smod = is_shortcut_mod(im, event->keysym.mod); @@ -573,8 +575,9 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, void input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event, - bool control) { + const SDL_MouseButtonEvent *event) { + bool control = im->control; + if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index c854664a..b3be6507 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -22,6 +22,7 @@ struct input_manager { // number of repetitions. This variable keeps track of the count. unsigned repeat; + bool control; bool prefer_text; struct { @@ -31,8 +32,8 @@ struct input_manager { }; void -input_manager_init(struct input_manager *im, bool prefer_text, - const struct sc_shortcut_mods *shortcut_mods); +input_manager_init(struct input_manager *im, + const struct scrcpy_options *options); void input_manager_process_text_input(struct input_manager *im, @@ -40,8 +41,7 @@ input_manager_process_text_input(struct input_manager *im, void input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event, - bool control); + const SDL_KeyboardEvent *event); void input_manager_process_mouse_motion(struct input_manager *im, @@ -53,8 +53,7 @@ input_manager_process_touch(struct input_manager *im, void input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event, - bool control); + const SDL_MouseButtonEvent *event); void input_manager_process_mouse_wheel(struct input_manager *im, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 06a3b3d1..f290f1c2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -201,7 +201,7 @@ handle_event(SDL_Event *event, bool control) { case SDL_KEYUP: // some key events do not interact with the device, so process the // event even if control is disabled - input_manager_process_key(&input_manager, &event->key, control); + input_manager_process_key(&input_manager, &event->key); break; case SDL_MOUSEMOTION: if (!control) { @@ -219,8 +219,7 @@ handle_event(SDL_Event *event, bool control) { case SDL_MOUSEBUTTONUP: // some mouse events do not interact with the device, so process // the event even if control is disabled - input_manager_process_mouse_button(&input_manager, &event->button, - control); + input_manager_process_mouse_button(&input_manager, &event->button); break; case SDL_FINGERMOTION: case SDL_FINGERDOWN: @@ -443,8 +442,7 @@ scrcpy(const struct scrcpy_options *options) { } } - input_manager_init(&input_manager, options->prefer_text, - &options->shortcut_mods); + input_manager_init(&input_manager, options); ret = event_loop(options->display, options->control); LOGD("quit..."); From 65d06a36634287426206b459d2f290142ea1bf46 Mon Sep 17 00:00:00 2001 From: xeropresence <3128949+xeropresence@users.noreply.github.com> Date: Mon, 27 Jul 2020 02:26:25 -0400 Subject: [PATCH 188/214] Pass full options struct to static functions This avoids to pass specific options values individually. Since these function are static (internal to the file), this is not a problem to make them depend on scrcpy_options. Refs #1623 Signed-off-by: Romain Vimont --- app/src/scrcpy.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f290f1c2..45068cbb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -170,7 +170,7 @@ enum event_result { }; static enum event_result -handle_event(SDL_Event *event, bool control) { +handle_event(SDL_Event *event, const struct scrcpy_options *options) { switch (event->type) { case EVENT_STREAM_STOPPED: LOGD("Video stream stopped"); @@ -192,7 +192,7 @@ handle_event(SDL_Event *event, bool control) { screen_handle_window_event(&screen, &event->window); break; case SDL_TEXTINPUT: - if (!control) { + if (!options->control) { break; } input_manager_process_text_input(&input_manager, &event->text); @@ -204,13 +204,13 @@ handle_event(SDL_Event *event, bool control) { input_manager_process_key(&input_manager, &event->key); break; case SDL_MOUSEMOTION: - if (!control) { + if (!options->control) { break; } input_manager_process_mouse_motion(&input_manager, &event->motion); break; case SDL_MOUSEWHEEL: - if (!control) { + if (!options->control) { break; } input_manager_process_mouse_wheel(&input_manager, &event->wheel); @@ -227,7 +227,7 @@ handle_event(SDL_Event *event, bool control) { input_manager_process_touch(&input_manager, &event->tfinger); break; case SDL_DROPFILE: { - if (!control) { + if (!options->control) { break; } file_handler_action_t action; @@ -244,16 +244,15 @@ handle_event(SDL_Event *event, bool control) { } static bool -event_loop(bool display, bool control) { - (void) display; +event_loop(const struct scrcpy_options *options) { #ifdef CONTINUOUS_RESIZING_WORKAROUND - if (display) { + if (options->display) { SDL_AddEventWatch(event_watcher, NULL); } #endif SDL_Event event; while (SDL_WaitEvent(&event)) { - enum event_result result = handle_event(&event, control); + enum event_result result = handle_event(&event, options); switch (result) { case EVENT_RESULT_STOPPED_BY_USER: return true; @@ -444,7 +443,7 @@ scrcpy(const struct scrcpy_options *options) { input_manager_init(&input_manager, options); - ret = event_loop(options->display, options->control); + ret = event_loop(options); LOGD("quit..."); screen_destroy(&screen); From 84f1d9e37502fbffc1e6b6754b930a37a5403703 Mon Sep 17 00:00:00 2001 From: xeropresence <3128949+xeropresence@users.noreply.github.com> Date: Mon, 27 Jul 2020 02:26:25 -0400 Subject: [PATCH 189/214] Add --no-key-repeat cli option Add an option to avoid forwarding repeated key events. PR #1623 Refs #1013 Signed-off-by: Romain Vimont --- README.md | 12 ++++++++++++ app/scrcpy.1 | 4 ++++ app/src/cli.c | 8 ++++++++ app/src/input_manager.c | 4 ++++ app/src/input_manager.h | 1 + app/src/scrcpy.h | 2 ++ 6 files changed, 31 insertions(+) diff --git a/README.md b/README.md index b3a5c6b6..d875fd99 100644 --- a/README.md +++ b/README.md @@ -558,6 +558,18 @@ scrcpy --prefer-text [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 +#### Key repeat + +By default, holding a key down generates repeated key events. This can cause +performance problems in some games, where these events are useless anyway. + +To avoid forwarding repeated key events: + +```bash +scrcpy --no-key-repeat +``` + + ### File drop #### Install APK diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 728d14fe..49fa78dc 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -96,6 +96,10 @@ Do not display device (only when screen recording is enabled). .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\-key\-repeat +Do not forward repeated key events when a key is held down. + .TP .BI "\-p, \-\-port " port[:port] Set the TCP port (range) used by the client to listen. diff --git a/app/src/cli.c b/app/src/cli.c index b75ab41a..c957b22f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -92,6 +92,9 @@ scrcpy_print_usage(const char *arg0) { " mipmaps are automatically generated to improve downscaling\n" " quality. This option disables the generation of mipmaps.\n" "\n" + " --no-key-repeat\n" + " Do not forward repeated key events when a key is held down.\n" + "\n" " -p, --port port[:port]\n" " Set the TCP port (range) used by the client to listen.\n" " Default is %d:%d.\n" @@ -642,6 +645,7 @@ guess_record_format(const char *filename) { #define OPT_FORCE_ADB_FORWARD 1019 #define OPT_DISABLE_SCREENSAVER 1020 #define OPT_SHORTCUT_MOD 1021 +#define OPT_NO_KEY_REPEAT 1022 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -664,6 +668,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, + {"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT}, {"port", required_argument, NULL, 'p'}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, @@ -829,6 +834,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_NO_MIPMAPS: opts->mipmaps = false; break; + case OPT_NO_KEY_REPEAT: + opts->forward_key_repeat = false; + break; case OPT_CODEC_OPTIONS: opts->codec_options = optarg; break; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3bf2282e..02dd9cb5 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -58,6 +58,7 @@ input_manager_init(struct input_manager *im, const struct scrcpy_options *options) { im->control = options->control; + im->forward_key_repeat = options->forward_key_repeat; im->prefer_text = options->prefer_text; const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; @@ -461,6 +462,9 @@ input_manager_process_key(struct input_manager *im, } if (event->repeat) { + if (!im->forward_key_repeat) { + return; + } ++im->repeat; } else { im->repeat = 0; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index b3be6507..8811c457 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -23,6 +23,7 @@ struct input_manager { unsigned repeat; bool control; + bool forward_key_repeat; bool prefer_text; struct { diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 05af002f..86a2b57b 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -78,6 +78,7 @@ struct scrcpy_options { bool stay_awake; bool force_adb_forward; bool disable_screensaver; + bool forward_key_repeat; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -121,6 +122,7 @@ struct scrcpy_options { .stay_awake = false, \ .force_adb_forward = false, \ .disable_screensaver = false, \ + .forward_key_repeat = true, \ } bool From cf9d44979cf4eca514a66cd9ec4d4bb8b5be1e19 Mon Sep 17 00:00:00 2001 From: brunoais Date: Wed, 8 Jul 2020 21:49:34 +0100 Subject: [PATCH 190/214] Keep the screen off on powering on PR #1577 Fixes #1573 Signed-off-by: Romain Vimont --- README.md | 8 ++++-- .../com/genymobile/scrcpy/Controller.java | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d875fd99..4ef28ab2 100644 --- a/README.md +++ b/README.md @@ -440,8 +440,12 @@ scrcpy -S Or by pressing MOD+o at any time. -To turn it back on, press MOD+Shift+o (or -`POWER`, MOD+p). +To turn it back on, press MOD+Shift+o. + +On Android, the `POWER` button always turns the screen on. For convenience, if +`POWER` is sent via scrcpy (via right-click or Ctrl+p), it +will force to turn the screen off after a small delay (on a best effort basis). +The physical `POWER` button will still cause the screen to be turned on. It can be useful to also prevent the device to sleep: diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index f32e5ec0..2ad26a95 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -8,11 +8,16 @@ import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class Controller { private static final int DEVICE_ID_VIRTUAL = -1; + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; @@ -24,6 +29,8 @@ public class Controller { private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; + private boolean keepPowerModeOff; + public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; @@ -117,6 +124,7 @@ public class Controller { int mode = msg.getAction(); boolean setPowerModeOk = Device.setScreenPowerMode(mode); if (setPowerModeOk) { + keepPowerModeOff = mode == Device.POWER_MODE_OFF; Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } } @@ -130,6 +138,9 @@ public class Controller { } private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { + if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + schedulePowerModeOff(); + } return device.injectKeyEvent(action, keycode, repeat, metaState); } @@ -223,8 +234,24 @@ public class Controller { return device.injectEvent(event); } + /** + * Schedule a call to set power mode to off after a small delay. + */ + private static void schedulePowerModeOff() { + EXECUTOR.schedule(new Runnable() { + @Override + public void run() { + Ln.i("Forcing screen off"); + Device.setScreenPowerMode(Device.POWER_MODE_OFF); + } + }, 200, TimeUnit.MILLISECONDS); + } + private boolean pressBackOrTurnScreenOn() { int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_WAKEUP; + if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_WAKEUP) { + schedulePowerModeOff(); + } return device.injectKeycode(keycode); } From 1ba06037f88aedf01bae1933b404eac1cb3aa129 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 6 Aug 2020 21:00:48 +0200 Subject: [PATCH 191/214] Upgrade platform-tools (30.0.4) for Windows Include the latest version of adb in Windows releases. --- prebuilt-deps/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index c6cbcf7b..b98864dc 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.12 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.0-windows.zip \ - 854305f9a702f5ea2c3de73edde402bd26afa0ee944c9b0c4380420f5a862e0d \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.4-windows.zip \ + 413182fff6c5957911e231b9e97e6be4fc6a539035e3dfb580b5c54bd5950fee \ platform-tools From 712f1fa6b2fa82f4916daad631bcae6c3de94e45 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 6 Aug 2020 21:00:48 +0200 Subject: [PATCH 192/214] Upgrade FFmpeg (4.3.1) for Windows Include the latest version of FFmpeg in Windows releases. --- Makefile.CrossWindows | 20 ++++++++++---------- cross_win32.txt | 4 ++-- cross_win64.txt | 4 ++-- prebuilt-deps/Makefile | 24 ++++++++++++------------ 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 3aed2019..0415af82 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,11 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -115,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/cross_win32.txt b/cross_win32.txt index 7b420690..bef2e5d5 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev' prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index cb9e0954..5a348738 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev' prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index b98864dc..9cccd0bd 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \ - ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \ - ffmpeg-4.2.2-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.3.1-win32-shared.zip \ + 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \ + ffmpeg-4.3.1-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \ - 8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \ - ffmpeg-4.2.2-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.3.1-win32-dev.zip \ + 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \ + ffmpeg-4.3.1-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \ - 5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \ - ffmpeg-4.2.2-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.3.1-win64-shared.zip \ + dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \ + ffmpeg-4.3.1-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \ - f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \ - ffmpeg-4.2.2-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.3.1-win64-dev.zip \ + 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ + ffmpeg-4.3.1-win64-dev prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ From edc4f7675f65d6c94a4c8935d13f84b6f601a79a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 6 Aug 2020 21:00:48 +0200 Subject: [PATCH 193/214] Bump version to 1.15 --- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 46b9a092..c4018189 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.14', + version: '1.15', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index c8ff85d6..9bcad1c1 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 16 - versionName "1.14" + versionCode 17 + versionName "1.15" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index e3be2faf..4fad829c 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=1.14 +SCRCPY_VERSION_NAME=1.15 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From 521f2fe994019065e938aa1a54b56b4f10a4ac4a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 6 Aug 2020 21:56:06 +0200 Subject: [PATCH 194/214] Update links to v1.15 in README and BUILD --- BUILD.md | 6 +++--- README.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BUILD.md b/BUILD.md index 57b2db97..c584afd1 100644 --- a/BUILD.md +++ b/BUILD.md @@ -254,10 +254,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.14`][direct-scrcpy-server] - _(SHA-256: 1d1b18a2b80e956771fd63b99b414d2d028713a8f12ddfa5a369709ad4295620)_ + - [`scrcpy-server-v1.15`][direct-scrcpy-server] + _(SHA-256: e160c46784f30566eae621cad88bfb34e124f945cb8274b8dfc615b613b86dd8)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-server-v1.14 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.15/scrcpy-server-v1.15 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 374d0821..0d6e2208 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.14) +# scrcpy (v1.15) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -74,10 +74,10 @@ hard). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.14.zip`][direct-win64] - _(SHA-256: 2be9139e46e29cf2f5f695848bb2b75a543b8f38be1133257dc5068252abc25f)_ + - [`scrcpy-win64-v1.15.zip`][direct-win64] + _(SHA-256: dd514bb591e63ef4cd52a53c30f1153a28f59722d64690eb07bd017849edcba2)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-win64-v1.14.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.15/scrcpy-win64-v1.15.zip It is also available in [Chocolatey]: From 976761956f5ff8a806a8547e7a1af9e4e7e3d77c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Aug 2020 09:21:13 +0200 Subject: [PATCH 195/214] Fix uninitialized repeat count in key events A new "repeat" field has been added by 3c1ed5d86c38bcbd5353b0a7e6ef5653d6c44d0d, but it was not initialized in every code path. As a consequence, keycodes generated by shortcuts were sent with an undetermined value, breaking some shortcuts (especially HOME) randomly. Fixes #1643 --- app/src/input_manager.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 02dd9cb5..1d73980c 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -80,6 +80,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode, msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.keycode = keycode; msg.inject_keycode.metastate = 0; + msg.inject_keycode.repeat = 0; if (actions & ACTION_DOWN) { msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; From 633a51e9c4c64ed549a9a44f8348ded528268451 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Aug 2020 12:01:34 +0200 Subject: [PATCH 196/214] Bump version to 1.15.1 --- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index c4018189..8492f520 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.15', + version: '1.15.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 9bcad1c1..c7c0abb0 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 17 - versionName "1.15" + versionCode 18 + versionName "1.15.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 4fad829c..cedbe186 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=1.15 +SCRCPY_VERSION_NAME=1.15.1 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From f03a3edde618402ecff442383845ec7ba7f0b829 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Aug 2020 12:13:27 +0200 Subject: [PATCH 197/214] Update links to v1.15.1 in README and BUILD --- BUILD.md | 6 +++--- README.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BUILD.md b/BUILD.md index c584afd1..2f1c2be9 100644 --- a/BUILD.md +++ b/BUILD.md @@ -254,10 +254,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.15`][direct-scrcpy-server] - _(SHA-256: e160c46784f30566eae621cad88bfb34e124f945cb8274b8dfc615b613b86dd8)_ + - [`scrcpy-server-v1.15.1`][direct-scrcpy-server] + _(SHA-256: fe06bd6a30da8c89860bf5e16eecce2b5054d4644c84289670ce00ca5d1637c3)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.15/scrcpy-server-v1.15 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.15.1/scrcpy-server-v1.15.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 0d6e2208..d45b6304 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.15) +# scrcpy (v1.15.1) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -74,10 +74,10 @@ hard). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.15.zip`][direct-win64] - _(SHA-256: dd514bb591e63ef4cd52a53c30f1153a28f59722d64690eb07bd017849edcba2)_ + - [`scrcpy-win64-v1.15.1.zip`][direct-win64] + _(SHA-256: 78fba4caad6328016ea93219254b5df391f24224c519a2c8e3f070695b8b38ff)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.15/scrcpy-win64-v1.15.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.15.1/scrcpy-win64-v1.15.1.zip It is also available in [Chocolatey]: From a2cb63e344077f7c34a71333217bc1bdf53ba633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jozef=20Holl=C3=BD?= Date: Sat, 8 Aug 2020 13:03:08 +0200 Subject: [PATCH 198/214] Add packaging status PR #1650 Signed-off-by: Romain Vimont --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d45b6304..5cf015cf 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ control it using keyboard and mouse. ## Get the app +Packaging status ### Linux From a59a15777de9c3af3da27d60181ae046a71c09f4 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sun, 9 Aug 2020 14:47:22 +0800 Subject: [PATCH 199/214] Fix missing change of Ctrl key in README PR #1652 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cf015cf..ee6978a2 100644 --- a/README.md +++ b/README.md @@ -449,7 +449,7 @@ Or by pressing MOD+o at any time. To turn it back on, press MOD+Shift+o. On Android, the `POWER` button always turns the screen on. For convenience, if -`POWER` is sent via scrcpy (via right-click or Ctrl+p), it +`POWER` is sent via scrcpy (via right-click or MOD+p), it will force to turn the screen off after a small delay (on a best effort basis). The physical `POWER` button will still cause the screen to be turned on. From 38940ffe892cab44a05d727be26b7a71e725bdba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Aug 2020 17:09:32 +0200 Subject: [PATCH 200/214] Revert "Inject WAKEUP instead of POWER" WAKEUP does not work on some devices. Fixes #1655 This reverts commit 322f1512ea806611eb37f508cd952bbbc050f853. --- .../src/main/java/com/genymobile/scrcpy/Controller.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 2ad26a95..9100a9db 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -55,10 +55,10 @@ public class Controller { public void control() throws IOException { // on start, power on the device if (!device.isScreenOn()) { - device.injectKeycode(KeyEvent.KEYCODE_WAKEUP); + device.injectKeycode(KeyEvent.KEYCODE_POWER); // dirty hack - // After the keycode is injected, the device is powered on asynchronously. + // After POWER is injected, the device is powered on asynchronously. // To turn the device screen off while mirroring, the client will send a message that // would be handled before the device is actually powered on, so its effect would // be "canceled" once the device is turned back on. @@ -248,8 +248,8 @@ public class Controller { } private boolean pressBackOrTurnScreenOn() { - int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_WAKEUP; - if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_WAKEUP) { + int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; + if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_POWER) { schedulePowerModeOff(); } return device.injectKeycode(keycode); From 95f1ea0d80e6bb61579ae6c9a50554d235d91383 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 10 Aug 2020 13:15:44 +0200 Subject: [PATCH 201/214] Fix clipboard paste condition To avoid possible copy-paste loops between the computer and the device, the device clipboard is not set if it already contains the expected content. But the condition was wrong: it was not set also if it was empty. Refs 1223a72eb83ebafb878a25356250896c45b5cefa Fixes #1658 --- server/src/main/java/com/genymobile/scrcpy/Device.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index de551f35..f23dd056 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -223,7 +223,7 @@ public final class Device { } String currentClipboard = getClipboardText(); - if (currentClipboard == null || currentClipboard.equals(text)) { + if (currentClipboard != null && currentClipboard.equals(text)) { // The clipboard already contains the requested text. // Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause // the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this From b87a0df99aa276673307f648853651158c2657b7 Mon Sep 17 00:00:00 2001 From: RaenonX JELLYCAT Date: Mon, 27 Jul 2020 09:14:15 -0500 Subject: [PATCH 202/214] Add Traditional Chinese translation for README Reviewed-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- README.md | 2 +- README.zh-Hant.md | 705 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 706 insertions(+), 1 deletion(-) create mode 100644 README.zh-Hant.md diff --git a/README.md b/README.md index ee6978a2..688f9356 100644 --- a/README.md +++ b/README.md @@ -733,4 +733,4 @@ Read the [developers page]. - [Scrcpy now works wirelessly][article-tcpip] [article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ -[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ \ No newline at end of file diff --git a/README.zh-Hant.md b/README.zh-Hant.md new file mode 100644 index 00000000..82eb1d9b --- /dev/null +++ b/README.zh-Hant.md @@ -0,0 +1,705 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +_只有原版的 [README](README.md)是保證最新的。_ + + +本文件翻譯時點: [521f2fe](https://github.com/Genymobile/scrcpy/commit/521f2fe994019065e938aa1a54b56b4f10a4ac4a#diff-04c6e90faac2675aa89e2176d2eec7d8) + + +# scrcpy (v1.15) + +Scrcpy 可以透過 USB、或是 [TCP/IP][article-tcpip] 來顯示或控制 Android 裝置。且 scrcpy 不需要 _root_ 權限。 + +Scrcpy 目前支援 _GNU/Linux_、_Windows_ 和 _macOS_。 + +![screenshot](assets/screenshot-debian-600.jpg) + +特色: + + - **輕量** (只顯示裝置螢幕) + - **效能** (30~60fps) + - **品質** (1920×1080 或更高) + - **低延遲** ([35~70ms][lowlatency]) + - **快速啟動** (~1 秒就可以顯示第一個畫面) + - **非侵入性** (不安裝、留下任何東西在裝置上) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## 需求 + +Android 裝置必須是 API 21+ (Android 5.0+)。 + +請確認裝置上的 [adb 偵錯 (通常位於開發者模式內)][enable-adb] 已啟用。 + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + + +在部分的裝置上,你也必須啟用[特定的額外選項][control]才能使用鍵盤和滑鼠控制。 + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## 下載/獲取軟體 + + +### Linux + +Debian (目前支援 _testing_ 和 _sid_) 和 Ubuntu (20.04): + +``` +apt install scrcpy +``` + +[Snap] 上也可以下載: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +在 Fedora 上也可以使用 [COPR] 下載: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +在 Arch Linux 上也可以使用 [AUR] 下載: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +在 Gentoo 上也可以使用 [Ebuild] 下載: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +你也可以自己[編譯 _Scrcpy_][BUILD]。別擔心,並沒有想像中的難。 + + + +### Windows + +為了保持簡單,Windows 用戶可以下載一個包含所有必需軟體 (包含 `adb`) 的壓縮包: + +- [`scrcpy-win64-v1.15.zip`][direct-win64] + _(SHA-256: dd514bb591e63ef4cd52a53c30f1153a28f59722d64690eb07bd017849edcba2)_ + +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.15/scrcpy-win64-v1.15.zip + +[Chocolatey] 上也可以下載: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # 如果你還沒有安裝的話 +``` + +[Scoop] 上也可以下載: + +```bash +scoop install scrcpy +scoop install adb # 如果你還沒有安裝的話 +``` + +[Scoop]: https://scoop.sh + +你也可以自己[編譯 _Scrcpy_][BUILD]。 + + +### macOS + +_Scrcpy_ 可以在 [Homebrew] 上直接安裝: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +由於執行期間需要可以藉由 `PATH` 存取 `adb` 。如果還沒有安裝 `adb` 可以使用下列方式安裝: + +```bash +brew cask install android-platform-tools +``` + +你也可以自己[編譯 _Scrcpy_][BUILD]。 + + +## 執行 + +將電腦和你的 Android 裝置連線,然後執行: + +```bash +scrcpy +``` + +_Scrcpy_ 可以接受命令列參數。輸入下列指令就可以瀏覽可以使用的命令列參數: + +```bash +scrcpy --help +``` + + +## 功能 + +> 以下說明中,有關快捷鍵的說明可能會出現 MOD 按鈕。相關說明請參見[快捷鍵]內的說明。 + +[快捷鍵]: #快捷鍵 + +### 畫面擷取 + +#### 縮小尺寸 + +使用比較低的解析度來投放 Android 裝置在某些情況可以提升效能。 + +限制寬和高的最大值(例如: 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # 縮短版本 +``` + +比較小的參數會根據螢幕比例重新計算。 +根據上面的範例,1920x1080 會被縮小成 1024x576。 + + +#### 更改 bit-rate + +預設的 bit-rate 是 8 Mbps。如果要更改 bit-rate (例如: 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # 縮短版本 +``` + +#### 限制 FPS + +限制畫面最高的 FPS: + +```bash +scrcpy --max-fps 15 +``` + +僅在 Android 10 後正式支援,不過也有可能可以在 Android 10 以前的版本使用。 + +#### 裁切 + +裝置的螢幕可以裁切。如此一來,鏡像出來的螢幕就只會是原本的一部份。 + +假如只要鏡像 Oculus Go 的其中一隻眼睛: + +```bash +scrcpy --crop 1224:1440:0:0 # 位於 (0,0),大小1224x1440 +``` + +如果 `--max-size` 也有指定的話,裁切後才會縮放。 + + +#### 鎖定影像方向 + + +如果要鎖定鏡像影像方向: + +```bash +scrcpy --lock-video-orientation 0 # 原本的方向 +scrcpy --lock-video-orientation 1 # 逆轉 90° +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 順轉 90° +``` + +這會影響錄影結果的影像方向。 + + +### 錄影 + +鏡像投放螢幕的同時也可以錄影: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +如果只要錄影,不要投放螢幕鏡像的話: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# 用 Ctrl+C 停止錄影 +``` + +就算有些幀為了效能而被跳過,它們還是一樣會被錄製。 + +裝置上的每一幀都有時間戳記,所以 [封包延遲 (Packet Delay Variation, PDV)][packet delay variation] 並不會影響錄影的檔案。 + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### 連線 + +#### 無線 + +_Scrcpy_ 利用 `adb` 和裝置通訊,而 `adb` 可以[透過 TCP/IP 連結][connect]: + +1. 讓電腦和裝置連到同一個 Wi-Fi。 +2. 獲取手機的 IP 位址(設定 → 關於手機 → 狀態). +3. 啟用裝置上的 `adb over TCP/IP`: `adb tcpip 5555`. +4. 拔掉裝置上的線。 +5. 透過 TCP/IP 連接裝置: `adb connect DEVICE_IP:5555` _(把 `DEVICE_IP` 換成裝置的IP位址)_. +6. 和平常一樣執行 `scrcpy`。 + +如果效能太差,可以降低 bit-rate 和解析度: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # 縮短版本 +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### 多裝置 + +如果 `adb devices` 內有多個裝置,則必須附上 _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # 縮短版本 +``` + +如果裝置是透過 TCP/IP 連線: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # 縮短版本 +``` + +你可以啟用復數個對應不同裝置的 _scrcpy_。 + +#### 裝置連結後自動啟動 + +你可以使用 [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### SSH tunnel + +本地的 `adb` 可以連接到遠端的 `adb` 伺服器(假設兩者使用相同版本的 _adb_ 通訊協定),以連接到遠端裝置: + +```bash +adb kill-server # 停止在 Port 5037 的本地 adb 伺服 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# 保持開啟 +``` + +從另外一個終端機: + +```bash +scrcpy +``` + +如果要避免啟用 remote port forwarding,你可以強制它使用 forward connection (注意 `-L` 和 `-R` 的差別): + +```bash +adb kill-server # 停止在 Port 5037 的本地 adb 伺服 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# 保持開啟 +``` + +從另外一個終端機: + +```bash +scrcpy --force-adb-forward +``` + + +和無線連接一樣,有時候降低品質會比較好: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### 視窗調整 + +#### 標題 + +預設標題是裝置的型號,不過可以透過以下方式修改: + +```bash +scrcpy --window-title 'My device' +``` + +#### 位置 & 大小 + +初始的視窗位置和大小也可以指定: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### 無邊框 + +如果要停用視窗裝飾: + +```bash +scrcpy --window-borderless +``` + +#### 保持最上層 + +如果要保持 `scrcpy` 的視窗在最上層: + +```bash +scrcpy --always-on-top +``` + +#### 全螢幕 + +這個軟體可以直接在全螢幕模式下起動: + +```bash +scrcpy --fullscreen +scrcpy -f # 縮短版本 +``` + +全螢幕可以使用 MOD+f 開關。 + +#### 旋轉 + +視窗可以旋轉: + +```bash +scrcpy --rotation 1 +``` + +可用的數值: + - `0`: 不旋轉 + - `1`: 90 度**逆**轉 + - `2`: 180 度 + - `3`: 90 度**順**轉 + +旋轉方向也可以使用 MOD+ _(左方向鍵)_ 和 MOD+ _(右方向鍵)_ 調整。 + +_scrcpy_ 有 3 種不同的旋轉: + - MOD+r 要求裝置在垂直、水平之間旋轉 (目前運行中的 App 有可能會因為不支援而拒絕)。 + - `--lock-video-orientation` 修改鏡像的方向 (裝置傳給電腦的影像)。這會影響錄影結果的影像方向。 + - `--rotation` (或是 MOD+ / MOD+) 只旋轉視窗的內容。這只會影響鏡像結果,不會影響錄影結果。 + + +### 其他鏡像選項 + +#### 唯讀 + +停用所有控制,包含鍵盤輸入、滑鼠事件、拖放檔案: + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### 顯示螢幕 + +如果裝置有複數個螢幕,可以指定要鏡像哪個螢幕: + +```bash +scrcpy --display 1 +``` + +可以透過下列指令獲取螢幕 ID: + +``` +adb shell dumpsys display # 找輸出結果中的 "mDisplayId=" +``` + +第二螢幕只有在 Android 10+ 時可以控制。如果不是 Android 10+,螢幕就會在唯讀狀態下投放。 + + +#### 保持清醒 + +如果要避免裝置在連接狀態下進入睡眠: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +_scrcpy_ 關閉後就會回復成原本的設定。 + + +#### 關閉螢幕 + +鏡像開始時,可以要求裝置關閉螢幕: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +或是在任何時候輸入 MOD+o。 + +如果要開啟螢幕,輸入 MOD+Shift+o。 + +在 Android 上,`POWER` 按鈕總是開啟螢幕。 + +為了方便,如果 `POWER` 是透過 scrcpy 轉送 (右鍵 或 MOD+p)的話,螢幕將會在短暫的延遲後關閉。 + +實際在手機上的 `POWER` 還是會開啟螢幕。 + +防止裝置進入睡眠狀態: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + + +#### 顯示過期的幀 + +為了降低延遲, _scrcpy_ 預設只會顯示最後解碼的幀,並且拋棄所有在這之前的幀。 + +如果要強制顯示所有的幀 (有可能會拉高延遲),輸入: + +```bash +scrcpy --render-expired-frames +``` + +#### 顯示觸控點 + +對於要報告的人來說,顯示裝置上的實際觸控點有時候是有幫助的。 + +Android 在_開發者選項_中有提供這個功能。 + +_Scrcpy_ 可以在啟動時啟用這個功能,並且在停止後恢復成原本的設定: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +這個選項只會顯示**實際觸碰在裝置上的觸碰點**。 + + +### 輸入控制 + + +#### 旋轉裝置螢幕 + +輸入 MOD+r 以在垂直、水平之間切換。 + +如果使用中的程式不支援,則不會切換。 + + +#### 複製/貼上 + +如果 Android 剪貼簿上的內容有任何更動,電腦的剪貼簿也會一起更動。 + +任何與 Ctrl 相關的快捷鍵事件都會轉送到裝置上。特別來說: + - Ctrl+c 通常是複製 + - Ctrl+x 通常是剪下 + - Ctrl+v 通常是貼上 (在電腦的剪貼簿與裝置上的剪貼簿同步之後) + +這些跟你通常預期的行為一樣。 + +但是,實際上的行為是根據目前運行中的應用程式而定。 + +舉例來說, _Termux_ 在收到 Ctrl+c 後,會傳送 SIGINT;而 _K-9 Mail_ 則是建立新訊息。 + +如果在這情況下,要剪下、複製或貼上 (只有在Android 7+時才支援): + - MOD+c 注入 `複製` + - MOD+x 注入 `剪下` + - MOD+v 注入 `貼上` (在電腦的剪貼簿與裝置上的剪貼簿同步之後) + +另外,MOD+Shift+v 則是以一連串的按鍵事件貼上電腦剪貼簿中的內容。當元件不允許文字貼上 (例如 _Termux_) 時,這就很有用。不過,這在非 ASCII 內容上就無法使用。 + +**警告:** 貼上電腦的剪貼簿內容 (無論是從 Ctrl+vMOD+v) 時,會複製剪貼簿中的內容至裝置的剪貼簿上。這會讓所有 Android 程式讀取剪貼簿的內容。請避免貼上任何敏感內容 (像是密碼)。 + + +#### 文字輸入偏好 + +輸入文字時,有兩種[事件][textevents]會被觸發: + - _鍵盤事件 (key events)_,代表有一個按鍵被按下或放開 + - _文字事件 (text events)_,代表有一個文字被輸入 + +預設上,文字是被以鍵盤事件 (key events) 輸入的,所以鍵盤和遊戲內所預期的一樣 (通常是指 WASD)。 + +但是這可能造成[一些問題][prefertext]。如果在這輸入這方面遇到了問題,你可以試試: + +```bash +scrcpy --prefer-text +``` + +(不過遊戲內鍵盤就會不可用) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### 重複輸入 + +通常來說,長時間按住一個按鍵會重複觸發按鍵事件。這會在一些遊戲中造成效能問題,而且這個重複的按鍵事件是沒有意義的。 + +如果不要轉送這些重複的按鍵事件: + +```bash +scrcpy --no-key-repeat +``` + + +### 檔案 + +#### 安裝 APK + +如果要安裝 APK ,拖放一個 APK 檔案 (以 `.apk` 為副檔名) 到 _scrcpy_ 的視窗上。 + +視窗上不會有任何反饋;結果會顯示在命令列中。 + + +#### 推送檔案至裝置 + +如果要推送檔案到裝置上的 `/sdcard/` ,拖放一個非 APK 檔案 (**不**以 `.apk` 為副檔名) 到 _scrcpy_ 的視窗上。 + +視窗上不會有任何反饋;結果會顯示在命令列中。 + +推送檔案的目標路徑可以在啟動時指定: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### 音訊轉送 + +_scrcpy_ **不**轉送音訊。請使用 [sndcpy]。另外,參見 [issue #14]。 + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## 快捷鍵 + +在以下的清單中,MOD 是快捷鍵的特殊按鍵。通常來說,這個按鍵是 (左) Alt 或是 (左) Super。 + +這個是可以使用 `--shortcut-mod` 更改的。可以用的選項有: +- `lctrl`: 左邊的 Ctrl +- `rctrl`: 右邊的 Ctrl +- `lalt`: 左邊的 Alt +- `ralt`: 右邊的 Alt +- `lsuper`: 左邊的 Super +- `rsuper`: 右邊的 Super + +```bash +# 以 右邊的 Ctrl 為快捷鍵特殊按鍵 +scrcpy --shortcut-mod=rctrl + +# 以 左邊的 Ctrl 和左邊的 Alt 或是 左邊的 Super 為快捷鍵特殊按鍵 +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] 通常是 WindowsCmd 鍵。_ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Action | Shortcut + | ------------------------------------------------- |:----------------------------- + | 切換至全螢幕 | MOD+f + | 左旋顯示螢幕 | MOD+ _(左)_ + | 右旋顯示螢幕 | MOD+ _(右)_ + | 縮放視窗成 1:1 (pixel-perfect) | MOD+g + | 縮放視窗到沒有黑邊框為止 | MOD+w \| _雙擊¹_ + | 按下 `首頁` 鍵 | MOD+h \| _中鍵_ + | 按下 `返回` 鍵 | MOD+b \| _右鍵²_ + | 按下 `切換 APP` 鍵 | MOD+s + | 按下 `選單` 鍵 (或解鎖螢幕) | MOD+m + | 按下 `音量+` 鍵 | MOD+ _(上)_ + | 按下 `音量-` 鍵 | MOD+ _(下)_ + | 按下 `電源` 鍵 | MOD+p + | 開啟 | _右鍵²_ + | 關閉裝置螢幕(持續鏡像) | MOD+o + | 開啟裝置螢幕 | MOD+Shift+o + | 旋轉裝置螢幕 | MOD+r + | 開啟通知列 | MOD+n + | 關閉通知列 | MOD+Shift+n + | 複製至剪貼簿³ | MOD+c + | 剪下至剪貼簿³ | MOD+x + | 同步剪貼簿並貼上³ | MOD+v + | 複製電腦剪貼簿中的文字至裝置並貼上 | MOD+Shift+v + | 啟用/停用 FPS 計數器(顯示於 stdout - 通常是命令列) | MOD+i + +_¹在黑邊框上雙擊以移除它們。_ +_²右鍵會返回。如果螢幕是關閉狀態,則會打開螢幕。_ +_³只支援 Android 7+。_ + +所有 Ctrl+_按鍵_ 快捷鍵都會傳送到裝置上,所以它們是由目前運作的應用程式處理的。 + + +## 自訂路徑 + +如果要使用特定的 _adb_ ,將它設定到環境變數中的 `ADB`: + + ADB=/path/to/adb scrcpy + +如果要覆寫 `scrcpy-server` 檔案的路徑,則將路徑設定到環境變數中的 `SCRCPY_SERVER_PATH`。 + +[相關連結][useful] + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## 為何叫 _scrcpy_ ? + +有一個同事要我找一個跟 [gnirehtet] 一樣難念的名字。 + +[`strcpy`] 複製一個字串 (**str**ing);`scrcpy` 複製一個螢幕 (**scr**een)。 + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## 如何編譯? + +請看[這份文件 (英文)][BUILD]。 + +[BUILD]: BUILD.md + + +## 常見問題 + +請看[這份文件 (英文)][FAQ]。 + +[FAQ]: FAQ.md + + +## 開發者文件 + +請看[這個頁面 (英文)][developers page]. + +[developers page]: DEVELOP.md + + +## Licence + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2020 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## 相關文章 + +- [Scrcpy 簡介 (英文)][article-intro] +- [Scrcpy 可以無線連線了 (英文)][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ From 8081e9cc11ac96b4dbe452991865d762f3dec468 Mon Sep 17 00:00:00 2001 From: RaenonX JELLYCAT Date: Mon, 27 Jul 2020 09:24:04 -0500 Subject: [PATCH 203/214] Add reference of the translations in README Reviewed-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 688f9356..0bec9ea6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # scrcpy (v1.15.1) +[Read in another language](#translations) + This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. It works on _GNU/Linux_, _Windows_ and _macOS_. @@ -733,4 +735,14 @@ Read the [developers page]. - [Scrcpy now works wirelessly][article-tcpip] [article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ -[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ \ No newline at end of file +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + +## Translations + +This README is available in other languages: + +- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) +- [한국어 (Korean, `ko`) - v1.11](README.ko.md) +- [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.12.1](README.pt-br.md) + +Only this README file is guaranteed to be up-to-date. From 198346d1482829119cf07c565086840254e519b9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Aug 2020 16:04:02 +0200 Subject: [PATCH 204/214] Add pinch-to-zoom simulation If Ctrl is hold when the left-click button is pressed, enable pinch-to-zoom to scale and rotate relative to the center of the screen. Fixes #24 --- README.md | 14 +++++++ app/scrcpy.1 | 4 ++ app/src/cli.c | 3 ++ app/src/control_msg.h | 1 + app/src/input_manager.c | 84 +++++++++++++++++++++++++++++++++++++---- app/src/input_manager.h | 2 + 6 files changed, 100 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ee6978a2..3f6d6b68 100644 --- a/README.md +++ b/README.md @@ -546,6 +546,19 @@ into the device clipboard. As a consequence, any Android application could read its content. You should avoid to paste sensitive content (like passwords) that way. + +#### Pinch-to-zoom + +To simulate "pinch-to-zoom": Ctrl+_click-and-move_. + +More precisely, hold Ctrl while pressing the left-click button. Until +the left-click button is released, all mouse movements scale and rotate the +content (if supported by the app) relative to the center of the screen. + +Concretely, scrcpy generates additional touch events from a "virtual finger" at +a location inverted through the center of the screen. + + #### Text injection preference There are two kinds of [events][textevents] generated when typing text: @@ -659,6 +672,7 @@ _[Super] is typically the Windows or Cmd key._ | Synchronize clipboards and paste³ | MOD+v | Inject computer clipboard text | MOD+Shift+v | Enable/disable FPS counter (on stdout) | MOD+i + | Pinch-to-zoom | Ctrl+_click-and-move_ _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 49fa78dc..4f3a8b9c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -316,6 +316,10 @@ Inject computer clipboard text as a sequence of key events .B MOD+i Enable/disable FPS counter (print frames/second in logs) +.TP +.B Ctrl+click-and-move +Pinch-to-zoom from the center of the screen + .TP .B Drag & drop APK file Install APK from computer diff --git a/app/src/cli.c b/app/src/cli.c index c957b22f..c960727e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -279,6 +279,9 @@ scrcpy_print_usage(const char *arg0) { " MOD+i\n" " Enable/disable FPS counter (print frames/second in logs)\n" "\n" + " Ctrl+click-and-move\n" + " Pinch-to-zoom from the center of the screen\n" + "\n" " Drag & drop APK file\n" " Install APK from computer\n" "\n", diff --git a/app/src/control_msg.h b/app/src/control_msg.h index e0b480de..6e3f239c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -17,6 +17,7 @@ #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6) #define POINTER_ID_MOUSE UINT64_C(-1); +#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2); enum control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 1d73980c..962db1d3 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -70,6 +70,8 @@ input_manager_init(struct input_manager *im, im->sdl_shortcut_mods.data[i] = sdl_mod; } im->sdl_shortcut_mods.count = shortcut_mods->count; + + im->vfinger_down = false; } static void @@ -299,6 +301,36 @@ input_manager_process_text_input(struct input_manager *im, } } +static bool +simulate_virtual_finger(struct input_manager *im, + enum android_motionevent_action action, + struct point point) { + bool up = action == AMOTION_EVENT_ACTION_UP; + + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + msg.inject_touch_event.action = action; + msg.inject_touch_event.position.screen_size = im->screen->frame_size; + msg.inject_touch_event.position.point = point; + msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER; + msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; + msg.inject_touch_event.buttons = 0; + + if (!controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'inject virtual finger event'"); + return false; + } + + return true; +} + +static struct point +inverse_point(struct point point, struct size size) { + point.x = size.width - point.x; + point.y = size.height - point.y; + return point; +} + static bool convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, bool prefer_text, uint32_t repeat) { @@ -512,10 +544,18 @@ input_manager_process_mouse_motion(struct input_manager *im, return; } struct control_msg msg; - if (convert_mouse_motion(event, im->screen, &msg)) { - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse motion event'"); - } + if (!convert_mouse_motion(event, im->screen, &msg)) { + return; + } + + if (!controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'inject mouse motion event'"); + } + + if (im->vfinger_down) { + struct point mouse = msg.inject_touch_event.position.point; + struct point vfinger = inverse_point(mouse, im->screen->frame_size); + simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } } @@ -587,7 +627,9 @@ input_manager_process_mouse_button(struct input_manager *im, // simulated from touch events, so it's a duplicate return; } - if (event->type == SDL_MOUSEBUTTONDOWN) { + + bool down = event->type == SDL_MOUSEBUTTONDOWN; + if (down) { if (control && event->button == SDL_BUTTON_RIGHT) { press_back_or_turn_screen_on(im->controller); return; @@ -618,10 +660,36 @@ input_manager_process_mouse_button(struct input_manager *im, } struct control_msg msg; - if (convert_mouse_button(event, im->screen, &msg)) { - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse button event'"); + if (!convert_mouse_button(event, im->screen, &msg)) { + return; + } + + if (!controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'inject mouse button event'"); + return; + } + + // Pinch-to-zoom simulation. + // + // If Ctrl is hold when the left-click button is pressed, then + // pinch-to-zoom mode is enabled: on every mouse event until the left-click + // button is released, an additional "virtual finger" event is generated, + // having a position inverted through the center of the screen. + // + // In other words, the center of the rotation/scaling is the center of the + // screen. +#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) + if ((down && !im->vfinger_down && CTRL_PRESSED) + || (!down && im->vfinger_down)) { + struct point mouse = msg.inject_touch_event.position.point; + struct point vfinger = inverse_point(mouse, im->screen->frame_size); + enum android_motionevent_action action = down + ? AMOTION_EVENT_ACTION_DOWN + : AMOTION_EVENT_ACTION_UP; + if (!simulate_virtual_finger(im, action, vfinger)) { + return; } + im->vfinger_down = down; } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 8811c457..c3756e40 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -30,6 +30,8 @@ struct input_manager { unsigned data[SC_MAX_SHORTCUT_MODS]; unsigned count; } sdl_shortcut_mods; + + bool vfinger_down; }; void From d7779d08e8f9004ec44197cd8eb7fdec0e1d4dd1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 10 Aug 2020 20:09:28 +0200 Subject: [PATCH 205/214] Bump version to 1.16 --- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 8492f520..11964cf6 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.15.1', + version: '1.16', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index c7c0abb0..c7f8bc0f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 18 - versionName "1.15.1" + versionCode 19 + versionName "1.16" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index cedbe186..e757be9a 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=1.15.1 +SCRCPY_VERSION_NAME=1.16 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From 479d10dc22b70272187e0963c6ad24d754a669a2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 10 Aug 2020 20:34:51 +0200 Subject: [PATCH 206/214] Update links to v1.16 in README and BUILD --- BUILD.md | 6 +++--- README.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BUILD.md b/BUILD.md index 2f1c2be9..a7c395ee 100644 --- a/BUILD.md +++ b/BUILD.md @@ -254,10 +254,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.15.1`][direct-scrcpy-server] - _(SHA-256: fe06bd6a30da8c89860bf5e16eecce2b5054d4644c84289670ce00ca5d1637c3)_ + - [`scrcpy-server-v1.16`][direct-scrcpy-server] + _(SHA-256: 94a79e05b4498d0460ab7bd9d12cbf05156e3a47bf0c5d1420cee1d4493b3832)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.15.1/scrcpy-server-v1.15.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-server-v1.16 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 55b0f127..e8b54465 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.15.1) +# scrcpy (v1.16) [Read in another language](#translations) @@ -77,10 +77,10 @@ hard). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.15.1.zip`][direct-win64] - _(SHA-256: 78fba4caad6328016ea93219254b5df391f24224c519a2c8e3f070695b8b38ff)_ + - [`scrcpy-win64-v1.16.zip`][direct-win64] + _(SHA-256: 3f30dc5db1a2f95c2b40a0f5de91ec1642d9f53799250a8c529bc882bc0918f0)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.15.1/scrcpy-win64-v1.15.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-win64-v1.16.zip It is also available in [Chocolatey]: From 6cc22e1c5b370baecee9072f52c44fdd13ba9634 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 Aug 2020 13:44:42 +0200 Subject: [PATCH 207/214] Reference --shortcut-mod from shortcuts list Fixes #1681 Suggested-by: Moritz Schulz --- app/scrcpy.1 | 2 +- app/src/cli.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 4f3a8b9c..6a890f14 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -222,7 +222,7 @@ Default is 0 (automatic).\n .SH SHORTCUTS In the following list, MOD is the shortcut modifier. By default, it's (left) -Alt or (left) Super, but it can be configured by \-\-shortcut-mod. +Alt or (left) Super, but it can be configured by \-\-shortcut-mod (see above). .TP .B MOD+f diff --git a/app/src/cli.c b/app/src/cli.c index c960727e..df84f6f5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -203,7 +203,7 @@ scrcpy_print_usage(const char *arg0) { "\n" " In the following list, MOD is the shortcut modifier. By default,\n" " it's (left) Alt or (left) Super, but it can be configured by\n" - " --shortcut-mod.\n" + " --shortcut-mod (see above).\n" "\n" " MOD+f\n" " Switch fullscreen mode\n" From d02789ce21a183682f968cc95dcd88c98efba0f0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 Aug 2020 13:52:01 +0200 Subject: [PATCH 208/214] List available shortcut keys on error Fixes #1681 Suggested-by: Moritz Schulz --- app/src/cli.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index df84f6f5..9c791fbf 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -532,7 +532,9 @@ parse_shortcut_mods_item(const char *item, size_t len) { } else if (STREQ("rsuper", item, key_len)) { mod |= SC_MOD_RSUPER; } else { - LOGW("Unknown modifier key: %.*s", (int) key_len, item); + LOGE("Unknown modifier key: %.*s " + "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", + (int) key_len, item); return 0; } #undef STREQ From 0be766e71a30eec1709da7b24999b6df2b40dc65 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 20 Aug 2020 19:14:45 +0200 Subject: [PATCH 209/214] Add undetected device error message in FAQ --- FAQ.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FAQ.md b/FAQ.md index 871ae8f0..ba33542e 100644 --- a/FAQ.md +++ b/FAQ.md @@ -37,6 +37,8 @@ Check [stackoverflow][device-unauthorized]. ### Device not detected +> adb: error: failed to get feature set: no devices/emulators found + If your device is not detected, you may need some [drivers] (on Windows). [drivers]: https://developer.android.com/studio/run/oem-usb.html From 52560faa344f19d6ec89a586ba85ab8958482e82 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Sep 2020 13:03:18 +0200 Subject: [PATCH 210/214] Fix README indentation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8b54465..da6833b5 100644 --- a/README.md +++ b/README.md @@ -382,9 +382,9 @@ The rotation can also be changed dynamically with MOD+ _(left)_ and MOD+ _(right)_. Note that _scrcpy_ manages 3 different rotations: -- MOD+r requests the device to switch between portrait and - landscape (the current running app may refuse, if it does support the - requested orientation). + - MOD+r requests the device to switch between portrait + and landscape (the current running app may refuse, if it does support the + requested orientation). - `--lock-video-orientation` changes the mirroring orientation (the orientation of the video sent from the device to the computer). This affects the recording. From c136edf09d54b4f61e27577622e7ba93f72acb0f Mon Sep 17 00:00:00 2001 From: Win7GM Date: Thu, 10 Sep 2020 12:18:56 +0800 Subject: [PATCH 211/214] Add simplified Chinese translation for README.md PR #1723 Co-authored-by: Shaw Yu Signed-off-by: Romain Vimont --- README.md | 3 +- README.zh-Hans.md | 726 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 728 insertions(+), 1 deletion(-) create mode 100644 README.zh-Hans.md diff --git a/README.md b/README.md index da6833b5..2c5816a9 100644 --- a/README.md +++ b/README.md @@ -755,8 +755,9 @@ Read the [developers page]. This README is available in other languages: -- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.12.1](README.pt-br.md) +- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.16](README.zh-Hans.md) +- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) Only this README file is guaranteed to be up-to-date. diff --git a/README.zh-Hans.md b/README.zh-Hans.md new file mode 100644 index 00000000..85e178d6 --- /dev/null +++ b/README.zh-Hans.md @@ -0,0 +1,726 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +只有原版的[README](README.md)会保持最新。 + +本文根据[479d10d]进行翻译。 + +[479d10d]: https://github.com/Genymobile/scrcpy/commit/479d10dc22b70272187e0963c6ad24d754a669a2#diff-04c6e90faac2675aa89e2176d2eec7d8 + + + +# scrcpy (v1.16) + +本应用程序可以通过USB(或 [TCP/IP][article-tcpip] )连接用于显示或控制安卓设备。这不需要获取 _root_ 权限。 + +该应用程序可以在 _GNU/Linux_, _Windows_ 和 _macOS_ 环境下运行。 + +[article-tcpip]:https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + +![screenshot](assets/screenshot-debian-600.jpg) + +它专注于: + + - **轻量** (原生,仅显示设备屏幕) + - **性能** (30~60fps) + - **质量** (分辨率可达1920x1080或更高) + - **低延迟** (35-70ms) + - **快速启动** (数秒内即能开始显示) + - **无侵入性** (不需要在安卓设备上安装任何程序) + + +## 使用要求 + +安卓设备系统版本需要在Android 5.0(API 21)或以上。 + +确保您在设备上开启了[adb调试]。 + +[adb调试]: https://developer.android.com/studio/command-line/adb.html#Enabling + +在某些设备上,你还需要开启[额外的选项]以用鼠标和键盘进行控制。 + +[额外的选项]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## 获取scrcpy + +Packaging status + +### Linux + +在Debian(目前仅测试版和不稳定版,即 _testing_ 和 _sid_ 版本)和Ubuntu (20.04)上: + +``` +apt install scrcpy +``` + +[Snap]包也是可用的: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +对于Fedora用户,我们提供[COPR]包: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +对于Arch Linux用户,我们提供[AUR]包: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +对于Gentoo用户,我们提供[Ebuild]包:[`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +您也可以[自行编译][编译](不必担心,这并不困难)。 + + + +### Windows + +在Windows上,简便起见,我们准备了包含所有依赖项(包括adb)的程序包。 + + - [`scrcpy-win64-v1.16.zip`][direct-win64] + _(SHA-256: 3f30dc5db1a2f95c2b40a0f5de91ec1642d9f53799250a8c529bc882bc0918f0)_ + +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-win64-v1.16.zip + +您也可以在[Chocolatey]下载: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # 如果你没有adb +``` + +也可以使用 [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # 如果你没有adb +``` + +[Scoop]: https://scoop.sh + +您也可以[自行编译][编译]。 + + +### macOS + +您可以使用[Homebrew]下载scrcpy。直接安装就可以了: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +您需要 `adb`以使用scrcpy,并且它需要可以通过 `PATH`被访问。如果您没有: + +```bash +brew cask install android-platform-tools +``` + +您也可以[自行编译][编译]。 + + +## 运行scrcpy + +用USB链接电脑和安卓设备,并执行: + +```bash +scrcpy +``` + +支持带命令行参数执行,查看参数列表: + +```bash +scrcpy --help +``` + +## 功能介绍 + +### 画面设置 + +#### 缩小分辨率 + +有时候,将设备屏幕镜像分辨率降低可以有效地提升性能。 + +我们可以将高度和宽度都限制在一定大小内(如 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # short version +``` + +较短的一边会被按比例缩小以保持设备的显示比例。 +这样,1920x1080 的设备会以 1024x576 的分辨率显示。 + + +#### 修改画面比特率 + +默认的比特率是8Mbps。如果要改变画面的比特率 (比如说改成2Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # short version +``` + +#### 限制画面帧率 + +画面的帧率可以通过下面的命令被限制: + +```bash +scrcpy --max-fps 15 +``` + +这个功能仅在Android 10和以后的版本被Android官方支持,但也有可能在更早的版本可用。 + +#### 画面裁剪 + +设备画面可在裁切后进行镜像,以显示部分屏幕。 + +这项功能可以用于,例如,只显示Oculus Go的一只眼睛。 + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +``` + +如果`--max-size`在同时被指定,分辨率的改变将在画面裁切后进行。 + + +#### 锁定屏幕朝向 + + +可以使用如下命令锁定屏幕朝向: + +```bash +scrcpy --lock-video-orientation 0 # 自然朝向 +scrcpy --lock-video-orientation 1 # 90° 逆时针旋转 +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° 顺时针旋转 +``` + +该设定影响录制。 + + +### 屏幕录制 + +可以在屏幕镜像的同时录制视频: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +在不开启屏幕镜像的同时录制: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# 按Ctrl+C以停止录制 +``` + +在显示中“被跳过的帧”会被录制,虽然它们由于性能原因没有实时显示。 +在传输中每一帧都有 _时间戳_ ,所以 [包时延变化] 并不影响录制的文件。 + +[包时延变化]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### 连接方式 + +#### 无线 + +_Scrcpy_ 使用`adb`来与安卓设备连接。同时,`adb`能够通过TCP/IP[连接]到安卓设备: + +1. 将您的安卓设备和电脑连接至同一Wi-Fi。 +2. 获取安卓设备的IP地址(在设置-关于手机-状态信息)。 +3. 打开安卓设备的网络adb功能`adb tcpip 5555`。 +4. 将您的设备与电脑断开连接。 +5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(用设备IP替换 `DEVICE_IP`)_. +6. 运行`scrcpy`。 + +降低比特率和分辨率可能有助于性能: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # short version +``` + +[连接]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### 多设备 + +如果多个设备在执行`adb devices`后被列出,您必须指定设备的 _序列号_ : + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # short version +``` + +如果设备是通过TCP/IP方式连接到电脑的: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # short version +``` + +您可以同时启动多个 _scrcpy_ 实例以同时显示多个设备的画面。 + +#### 在设备连接时自动启动 + +您可以使用 [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### SSH 连接 + +本地的 adb 可以远程连接到另一个 adb 服务器(假设两者的adb版本相同),来远程连接到设备: + +```bash +adb kill-server # 关闭本地5037端口上的adb服务器 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# 保持该窗口开启 +``` + +从另一个终端: + +```bash +scrcpy +``` + +为了避免启动远程端口转发,你可以强制启动一个转发连接(注意`-L`和`-R`的区别: + +```bash +adb kill-server # kill the local adb server on 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# 保持该窗口开启 +``` + +从另一个终端: + +```bash +scrcpy --force-adb-forward +``` + + +和无线网络连接类似,下列设置可能对改善性能有帮助: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### 窗口设置 + +#### 标题 + +窗口的标题默认为设备型号。您可以通过如下命令修改它: + +```bash +scrcpy --window-title 'My device' +``` + +#### 位置和大小 + +您可以指定初始的窗口位置和大小: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### 无边框 + +关闭边框: + +```bash +scrcpy --window-borderless +``` + +#### 保持窗口在最前 + +您可以通过如下命令保持窗口在最前面: + +```bash +scrcpy --always-on-top +``` + +#### 全屏 + +您可以通过如下命令直接全屏启动scrcpy: + +```bash +scrcpy --fullscreen +scrcpy -f # short version +``` + +全屏状态可以通过MOD+f实时改变。 + +#### 旋转 + +通过如下命令,窗口可以旋转: + +```bash +scrcpy --rotation 1 +``` + +可选的值有: + - `0`: 无旋转 + - `1`: 逆时针旋转90° + - `2`: 旋转180° + - `3`: 顺时针旋转90° + +这同样可以使用MOD+ +_(左)_ 和 MOD+ _(右)_ 的快捷键实时更改。 + +需要注意的是, _scrcpy_ 控制三个不同的朝向: + - MOD+r 请求设备在竖屏和横屏之间切换(如果前台应用程序不支持所请求的朝向,可能会拒绝该请求)。 + + - `--lock-video-orientation` 改变镜像的朝向(设备镜像到电脑的画面朝向)。这会影响录制。 + + - `--rotation` (或MOD+/MOD+) + 只旋转窗口的画面。这只影响显示,不影响录制。 + + +### 其他镜像设置 + +#### 只读 + +关闭电脑对设备的控制(如键盘输入、鼠标移动和文件传输): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### 显示屏 + +如果有多个显示屏可用,您可以选择特定显示屏进行镜像: + +```bash +scrcpy --display 1 +``` + +您可以通过如下命令找到显示屏的id: + +``` +adb shell dumpsys display # 在回显中搜索“mDisplayId=” +``` + +第二显示屏可能只能在设备运行Android 10或以上的情况下被控制(它可能会在电脑上显示,但无法通过电脑操作)。 + + +#### 保持常亮 + +防止设备在已连接的状态下休眠: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +程序关闭后,设备设置会恢复原样。 + + +#### 关闭设备屏幕 + +在启动屏幕镜像时,可以通过如下命令关闭设备的屏幕: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +或者在需要的时候按MOD+o。 + +要重新打开屏幕的话,需要按MOD+Shift+o. + +在Android上,`电源`按钮始终能把屏幕打开。 + +为了方便,如果按下`电源`按钮的事件是通过 _scrcpy_ 发出的(通过点按鼠标右键或MOD+p),它会在短暂的延迟后将屏幕关闭。 + +物理的`电源`按钮仍然能打开设备屏幕。 + +同时,这项功能还能被用于防止设备休眠: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + + +#### 渲染超时帧 + +为了降低延迟, _scrcpy_ 默认渲染解码成功的最近一帧,并跳过前面任意帧。 + +强制渲染所有帧(可能导致延迟变高): + +```bash +scrcpy --render-expired-frames +``` + +#### 显示触摸 + +在展示时,有些时候可能会用到显示触摸点这项功能(在设备上显示)。 + +Android在 _开发者设置_ 中提供了这项功能。 + +_Scrcpy_ 提供一个选项可以在启动时开启这项功能并在退出时恢复初始设置: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +请注意这项功能只能显示 _物理_ 触摸(要用手在屏幕上触摸)。 + + +#### 关闭屏保 + +_Scrcpy_ 不会默认关闭屏幕保护。 + +关闭屏幕保护: + +```bash +scrcpy --disable-screensaver +``` + + +### 输入控制 + +#### 旋转设备屏幕 + +使用MOD+r以在竖屏和横屏模式之间切换。 + +需要注意的是,只有在前台应用程序支持所要求的模式时,才会进行切换。 + +#### 复制黏贴 + +每次Android的剪贴板变化的时候,它都会被自动同步到电脑的剪贴板上。 + +所有的 Ctrl 快捷键都会被转发至设备。其中: + - Ctrl+c 复制 + - Ctrl+x 剪切 + - Ctrl+v 黏贴 (在电脑到设备的剪贴板同步完成之后) + +这通常如您所期望的那样运作。 + +但实际的行为取决于设备上的前台程序。 +例如 _Termux_ 在Ctrl+c被按下时发送 SIGINT, +又如 _K-9 Mail_ 会新建一封新邮件。 + +在这种情况下剪切复制黏贴(仅在Android >= 7时可用): + - MOD+c 注入 `COPY`(复制) + - MOD+x 注入 `CUT`(剪切) + - MOD+v 注入 `PASTE`(黏贴)(在电脑到设备的剪贴板同步完成之后) + +另外,MOD+Shift+v可以将电脑的剪贴板内容转换为一串按键事件输入到设备。 +在应用程序不接受黏贴时(比如 _Termux_ ),这项功能可以排上一定的用场。 +需要注意的是,这项功能可能会导致非ASCII编码的内容出现错误。 + +**警告:** 将电脑剪贴板的内容黏贴至设备(无论是通过Ctrl+v还是MOD+v) +都需要将内容保存至设备的剪贴板。如此,任何一个应用程序都可以读取它。 +您应当避免将敏感内容通过这种方式传输(如密码)。 + + +#### 捏拉缩放 + +模拟 “捏拉缩放”:Ctrl+_按住并移动鼠标_。 + +更准确的说,您需要在按住Ctrl的同时按住并移动鼠标。 +在鼠标左键松开之后,光标的任何操作都会相对于屏幕的中央进行。 + +具体来说, _scrcpy_ 使用“虚拟手指”以在相对于屏幕中央相反的位置产生触摸事件。 + + +#### 文字注入偏好 + +打字的时候,系统会产生两种[事件][textevents]: + - _按键事件_ ,代表一个按键被按下/松开。 + - _文本事件_ ,代表一个文本被输入。 + +程序默认使用按键事件来输入字母。只有这样,键盘才会在游戏中正常运作(尤其WASD键)。 + +但这也有可能[造成问题][prefertext]。如果您遇到了这样的问题,您可以通过下列操作避免它: + +```bash +scrcpy --prefer-text +``` + +(这会导致键盘在游戏中工作不正常) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### 按键重复 + +当你一直按着一个按键不放时,程序默认产生多个按键事件。 +在某些游戏中这可能会导致性能问题。 + +避免转发重复按键事件: + +```bash +scrcpy --no-key-repeat +``` + + +### 文件传输 + +#### 安装APK + +如果您要要安装APK,请拖放APK文件(文件名以`.apk`结尾)到 _scrcpy_ 窗口。 + +该操作在屏幕上不会出现任何变化,而会在控制台输出一条日志。 + + +#### 将文件推送至设备 + +如果您要推送文件到设备的 `/sdcard/`,请拖放文件至(不能是APK文件)_scrcpy_ 窗口。 + +该操作没有可见的响应,只会在控制台输出日志。 + +在启动时可以修改目标目录: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### 音频转发 + +_scrcpy_ 不支持音频。请使用 [sndcpy]. + +另外请阅读 [issue #14]。 + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## 热键 + +在下列表格中, MOD 是热键的修饰键。 +默认是(左)Alt或者(左)Super。 + +您可以使用 `--shortcut-mod`后缀来修改它。可选的按键有`lctrl`、`rctrl`、 +`lalt`、`ralt`、`lsuper`和`rsuper`。如下例: + +```bash +# 使用右侧的Ctrl键 +scrcpy --shortcut-mod=rctrl + +# 使用左侧的Ctrl键、Alt键或Super键 +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_一般来说,[Super]就是Windows或者Cmd。_ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | 操作 | 快捷键 + | ------------------------------------------- |:----------------------------- + | 全屏 | MOD+f + | 向左旋转屏幕 | MOD+ _(左)_ + | 向右旋转屏幕 | MOD+ _(右)_ + | 将窗口大小重置为1:1 (像素优先) | MOD+g + | 将窗口大小重置为消除黑边 | MOD+w \| _双击¹_ + | 点按 `主屏幕` | MOD+h \| _点击鼠标中键_ + | 点按 `返回` | MOD+b \| _点击鼠标右键²_ + | 点按 `切换应用` | MOD+s + | 点按 `菜单` (解锁屏幕) | MOD+m + | 点按 `音量+` | MOD+ _(up)_ + | 点按 `音量-` | MOD+ _(down)_ + | 点按 `电源` | MOD+p + | 打开屏幕 | _点击鼠标右键²_ + | 关闭设备屏幕(但继续在电脑上显示) | MOD+o + | 打开设备屏幕 | MOD+Shift+o + | 旋转设备屏幕 | MOD+r + | 展开通知面板 | MOD+n + | 展开快捷操作 | MOD+Shift+n + | 复制到剪贴板³ | MOD+c + | 剪切到剪贴板³ | MOD+x + | 同步剪贴板并黏贴³ | MOD+v + | 导入电脑剪贴板文本 | MOD+Shift+v + | 打开/关闭FPS显示(在 stdout) | MOD+i + | 捏拉缩放 | Ctrl+_点按并移动鼠标_ + +_¹双击黑色边界以关闭黑色边界_ +_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下 返回键 。_ +_³需要安卓版本 Android >= 7。_ + +所有的 Ctrl+_按键_ 的热键都是被转发到设备进行处理的,所以实际上会由当前应用程序对其做出响应。 + + +## 自定义路径 + +为了使用您想使用的 _adb_ ,您可以在环境变量 +`ADB`中设置它的路径: + + ADB=/path/to/adb scrcpy + +如果需要覆盖`scrcpy-server`的路径,您可以在 +`SCRCPY_SERVER_PATH`中设置它。 + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## 为什么叫 _scrcpy_ ? + +一个同事让我找出一个和[gnirehtet]一样难以发音的名字。 + +[`strcpy`] 可以复制**str**ing; `scrcpy` 可以复制**scr**een。 + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## 如何编译? + +请查看[编译]。 + +[编译]: BUILD.md + + +## 常见问题 + +请查看[FAQ](FAQ.md). + + +## 开发者 + +请查看[开发者页面]。 + +[开发者页面]: DEVELOP.md + + +## 许可协议 + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2020 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## 相关文章 + +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ From 15b81367c973a4da7285c697ca2a04d87f44d656 Mon Sep 17 00:00:00 2001 From: redev <58584157+redev-5c@users.noreply.github.com> Date: Thu, 17 Sep 2020 09:16:48 +0900 Subject: [PATCH 212/214] Fix FAQ.ko.md PR #1767 Signed-off-by: Romain Vimont --- FAQ.ko.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FAQ.ko.md b/FAQ.ko.md index 6cc1a1d9..c9e06e24 100644 --- a/FAQ.ko.md +++ b/FAQ.ko.md @@ -3,16 +3,16 @@ 다음은 자주 제보되는 문제들과 그들의 현황입니다. -### Window 운영체제에서, 디바이스가 발견되지 않습니다. +### Windows 운영체제에서, 디바이스가 발견되지 않습니다. 가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다. 다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요: adb devices -Window는 당신의 디바이스를 감지하기 위해 [drivers]가 필요할 수도 있습니다. +Windows는 당신의 디바이스를 감지하기 위해 [드라이버]가 필요할 수도 있습니다. -[drivers]: https://developer.android.com/studio/run/oem-usb.html +[드라이버]: https://developer.android.com/studio/run/oem-usb.html ### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다. From aade92fd1058a88cfdcacf168f2b2b8857b5d562 Mon Sep 17 00:00:00 2001 From: yanuarakhid Date: Fri, 2 Oct 2020 00:50:38 +0700 Subject: [PATCH 213/214] Add Indonesian translation for README.md PR #1802 Signed-off-by: Romain Vimont --- README.id.md | 699 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 700 insertions(+) create mode 100644 README.id.md diff --git a/README.id.md b/README.id.md new file mode 100644 index 00000000..5af56663 --- /dev/null +++ b/README.id.md @@ -0,0 +1,699 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +# scrcpy (v1.16) + +Aplikasi ini menyediakan tampilan dan kontrol perangkat Android yang terhubung pada USB (atau [melalui TCP/IP][article-tcpip]). Ini tidak membutuhkan akses _root_ apa pun. Ini bekerja pada _GNU/Linux_, _Windows_ and _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Ini berfokus pada: + + - **keringanan** (asli, hanya menampilkan layar perangkat) + - **kinerja** (30~60fps) + - **kualitas** (1920×1080 atau lebih) + - **latensi** rendah ([35~70ms][lowlatency]) + - **waktu startup rendah** (~1 detik untuk menampilkan gambar pertama) + - **tidak mengganggu** (tidak ada yang terpasang di perangkat) + + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Persyaratan +Perangkat Android membutuhkan setidaknya API 21 (Android 5.0). + +Pastikan Anda [mengaktifkan debugging adb][enable-adb] pada perangkat Anda. + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +Di beberapa perangkat, Anda juga perlu mengaktifkan [opsi tambahan][control] untuk mengontrolnya menggunakan keyboard dan mouse. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Dapatkan aplikasinya + +### Linux + +Di Debian (_testing_ dan _sid_ untuk saat ini) dan Ubuntu (20.04): + +``` +apt install scrcpy +``` + +Paket [Snap] tersedia: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Untuk Fedora, paket [COPR] tersedia: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Untuk Arch Linux, paket [AUR] tersedia: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Untuk Gentoo, tersedia [Ebuild]: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +Anda juga bisa [membangun aplikasi secara manual][BUILD] (jangan khawatir, tidak terlalu sulit). + + +### Windows + +Untuk Windows, untuk kesederhanaan, arsip prebuilt dengan semua dependensi (termasuk `adb`) tersedia : + + - [`scrcpy-win64-v1.16.zip`][direct-win64] + _(SHA-256: 3f30dc5db1a2f95c2b40a0f5de91ec1642d9f53799250a8c529bc882bc0918f0)_ + +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-win64-v1.16.zip + +Ini juga tersedia di [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # jika Anda belum memilikinya +``` + +Dan di [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # jika Anda belum memilikinya +``` + +[Scoop]: https://scoop.sh + +Anda juga dapat [membangun aplikasi secara manual][BUILD]. + + +### macOS + +Aplikasi ini tersedia di [Homebrew]. Instal saja: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` +Anda membutuhkan `adb`, dapat diakses dari `PATH` Anda. Jika Anda belum memilikinya: + +```bash +brew cask install android-platform-tools +``` + +Anda juga dapat [membangun aplikasi secara manual][BUILD]. + + +## Menjalankan + +Pasang perangkat Android, dan jalankan: + +```bash +scrcpy +``` + +Ini menerima argumen baris perintah, didaftarkan oleh: + +```bash +scrcpy --help +``` + +## Fitur + +### Menangkap konfigurasi + +#### Mengurangi ukuran + +Kadang-kadang, berguna untuk mencerminkan perangkat Android dengan definisi yang lebih rendah untuk meningkatkan kinerja. + +Untuk membatasi lebar dan tinggi ke beberapa nilai (mis. 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versi pendek +``` + +Dimensi lain dihitung agar rasio aspek perangkat dipertahankan. +Dengan begitu, perangkat 1920×1080 akan dicerminkan pada 1024×576. + +#### Ubah kecepatan bit + +Kecepatan bit default adalah 8 Mbps. Untuk mengubah bitrate video (mis. Menjadi 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versi pendek +``` + +#### Batasi frekuensi gambar + +Kecepatan bingkai pengambilan dapat dibatasi: + +```bash +scrcpy --max-fps 15 +``` + +Ini secara resmi didukung sejak Android 10, tetapi dapat berfungsi pada versi sebelumnya. + +#### Memotong + +Layar perangkat dapat dipotong untuk mencerminkan hanya sebagian dari layar. + +Ini berguna misalnya untuk mencerminkan hanya satu mata dari Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 Mengimbangi (0,0) +``` + +Jika `--max-size` juga ditentukan, pengubahan ukuran diterapkan setelah pemotongan. + + +#### Kunci orientasi video + +Untuk mengunci orientasi pencerminan: + +```bash +scrcpy --lock-video-orientation 0 # orientasi alami +scrcpy --lock-video-orientation 1 # 90° berlawanan arah jarum jam +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° searah jarum jam +``` + +Ini mempengaruhi orientasi perekaman. + + +### Rekaman + +Anda dapat merekam layar saat melakukan mirroring: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Untuk menonaktifkan pencerminan saat merekam: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# berhenti merekam dengan Ctrl+C +``` + +"Skipped frames" are recorded, even if they are not displayed in real time (for +performance reasons). Frames are _timestamped_ on the device, so [packet delay +variation] does not impact the recorded file. + +"Frame yang dilewati" direkam, meskipun tidak ditampilkan secara real time (untuk alasan performa). Bingkai *diberi stempel waktu* pada perangkat, jadi [variasi penundaan paket] tidak memengaruhi file yang direkam. + +[variasi penundaan paket]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Koneksi + +#### Wireless + +_Scrcpy_ menggunakan `adb` untuk berkomunikasi dengan perangkat, dan` adb` dapat [terhubung] ke perangkat melalui TCP / IP: + +1. Hubungkan perangkat ke Wi-Fi yang sama dengan komputer Anda. +2. Dapatkan alamat IP perangkat Anda (dalam Pengaturan → Tentang ponsel → Status). +3. Aktifkan adb melalui TCP / IP pada perangkat Anda: `adb tcpip 5555`. +4. Cabut perangkat Anda. +5. Hubungkan ke perangkat Anda: `adb connect DEVICE_IP: 5555` (*ganti* *`DEVICE_IP`*). +6. Jalankan `scrcpy` seperti biasa. + +Mungkin berguna untuk menurunkan kecepatan bit dan definisi: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versi pendek +``` + +[terhubung]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Multi-perangkat + +Jika beberapa perangkat dicantumkan di `adb devices`, Anda harus menentukan _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versi pendek +``` + +If the device is connected over TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versi pendek +``` + +Anda dapat memulai beberapa contoh _scrcpy_ untuk beberapa perangkat. + +#### Mulai otomatis pada koneksi perangkat + +Anda bisa menggunakan [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Koneksi via SSH tunnel + +Untuk menyambung ke perangkat jarak jauh, dimungkinkan untuk menghubungkan klien `adb` lokal ke server `adb` jarak jauh (asalkan mereka menggunakan versi yang sama dari _adb_ protocol): + +```bash +adb kill-server # matikan server adb lokal di 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 komputer_jarak_jauh_anda +# jaga agar tetap terbuka +``` + +Dari terminal lain: + +```bash +scrcpy +``` + +Untuk menghindari mengaktifkan penerusan port jarak jauh, Anda dapat memaksa sambungan maju sebagai gantinya (perhatikan `-L`, bukan` -R`): + +```bash +adb kill-server # matikan server adb lokal di 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 komputer_jarak_jauh_anda +# jaga agar tetap terbuka +``` + +Dari terminal lain: + +```bash +scrcpy --force-adb-forward +``` + +Seperti koneksi nirkabel, mungkin berguna untuk mengurangi kualitas: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Konfigurasi Jendela + +#### Judul + +Secara default, judul jendela adalah model perangkat. Itu bisa diubah: + +```bash +scrcpy --window-title 'Perangkat Saya' +``` + +#### Posisi dan ukuran + +Posisi dan ukuran jendela awal dapat ditentukan: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Jendela tanpa batas + +Untuk menonaktifkan dekorasi jendela: + +```bash +scrcpy --window-borderless +``` + +#### Selalu di atas + +Untuk menjaga jendela scrcpy selalu di atas: + +```bash +scrcpy --always-on-top +``` + +#### Layar penuh + +Aplikasi dapat dimulai langsung dalam layar penuh:: + +```bash +scrcpy --fullscreen +scrcpy -f # versi pendek +``` + +Layar penuh kemudian dapat diubah secara dinamis dengan MOD+f. + +#### Rotasi + +Jendela mungkin diputar: + +```bash +scrcpy --rotation 1 +``` + +Nilai yang mungkin adalah: + - `0`: tidak ada rotasi + - `1`: 90 derajat berlawanan arah jarum jam + - `2`: 180 derajat + - `3`: 90 derajat searah jarum jam + +Rotasi juga dapat diubah secara dinamis dengan MOD+ +_(kiri)_ and MOD+ _(kanan)_. + +Perhatikan bahwa _scrcpy_ mengelola 3 rotasi berbeda:: + - MOD+r meminta perangkat untuk beralih antara potret dan lanskap (aplikasi yang berjalan saat ini mungkin menolak, jika mendukung orientasi yang diminta). + - `--lock-video-orientation` mengubah orientasi pencerminan (orientasi video yang dikirim dari perangkat ke komputer). Ini mempengaruhi rekaman. + - `--rotation` (atau MOD+/MOD+) + memutar hanya konten jendela. Ini hanya mempengaruhi tampilan, bukan rekaman. + + +### Opsi pencerminan lainnya + +#### Hanya-baca + +Untuk menonaktifkan kontrol (semua yang dapat berinteraksi dengan perangkat: tombol input, peristiwa mouse, seret & lepas file): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Layar + +Jika beberapa tampilan tersedia, Anda dapat memilih tampilan untuk cermin: + +```bash +scrcpy --display 1 +``` + +Daftar id tampilan dapat diambil dengan:: + +``` +adb shell dumpsys display # cari "mDisplayId=" di keluaran +``` + +Tampilan sekunder hanya dapat dikontrol jika perangkat menjalankan setidaknya Android 10 (jika tidak maka akan dicerminkan dalam hanya-baca). + + +#### Tetap terjaga + +Untuk mencegah perangkat tidur setelah beberapa penundaan saat perangkat dicolokkan: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +Keadaan awal dipulihkan ketika scrcpy ditutup. + + +#### Matikan layar + +Dimungkinkan untuk mematikan layar perangkat saat pencerminan mulai dengan opsi baris perintah: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Atau dengan menekan MOD+o kapan saja. + +Untuk menyalakannya kembali, tekan MOD+Shift+o. + +Di Android, tombol `POWER` selalu menyalakan layar. Untuk kenyamanan, jika `POWER` dikirim melalui scrcpy (melalui klik kanan atauMOD+p), itu akan memaksa untuk mematikan layar setelah penundaan kecil (atas dasar upaya terbaik). +Tombol fisik `POWER` masih akan menyebabkan layar dihidupkan. + +Ini juga berguna untuk mencegah perangkat tidur: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + +#### Render frame kedaluwarsa + +Secara default, untuk meminimalkan latensi, _scrcpy_ selalu menampilkan frame yang terakhir didekodekan tersedia, dan menghapus frame sebelumnya. + +Untuk memaksa rendering semua frame (dengan kemungkinan peningkatan latensi), gunakan: + +```bash +scrcpy --render-expired-frames +``` + +#### Tunjukkan sentuhan + +Untuk presentasi, mungkin berguna untuk menunjukkan sentuhan fisik (pada perangkat fisik). + +Android menyediakan fitur ini di _Opsi Pengembang_. + +_Scrcpy_ menyediakan opsi untuk mengaktifkan fitur ini saat mulai dan mengembalikan nilai awal saat keluar: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Perhatikan bahwa ini hanya menunjukkan sentuhan _fisik_ (dengan jari di perangkat). + + +#### Nonaktifkan screensaver + +Secara default, scrcpy tidak mencegah screensaver berjalan di komputer. + +Untuk menonaktifkannya: + +```bash +scrcpy --disable-screensaver +``` + + +### Kontrol masukan + +#### Putar layar perangkat + +Tekan MOD+r untuk beralih antara mode potret dan lanskap. + +Perhatikan bahwa itu berputar hanya jika aplikasi di latar depan mendukung orientasi yang diminta. + +#### Salin-tempel + +Setiap kali papan klip Android berubah, secara otomatis disinkronkan ke papan klip komputer. + +Apa saja Ctrl pintasan diteruskan ke perangkat. Khususnya: + - Ctrl+c biasanya salinan + - Ctrl+x biasanya memotong + - Ctrl+v biasanya menempel (setelah sinkronisasi papan klip komputer-ke-perangkat) + +Ini biasanya berfungsi seperti yang Anda harapkan. + +Perilaku sebenarnya tergantung pada aplikasi yang aktif. Sebagai contoh, +_Termux_ mengirim SIGINT ke Ctrl+c sebagai gantinya, dan _K-9 Mail_ membuat pesan baru. + +Untuk menyalin, memotong dan menempel dalam kasus seperti itu (tetapi hanya didukung di Android> = 7): + - MOD+c injeksi `COPY` _(salin)_ + - MOD+x injeksi `CUT` _(potong)_ + - MOD+v injeksi `PASTE` (setelah sinkronisasi papan klip komputer-ke-perangkat) + +Tambahan, MOD+Shift+v memungkinkan untuk memasukkan teks papan klip komputer sebagai urutan peristiwa penting. Ini berguna ketika komponen tidak menerima penempelan teks (misalnya di _Termux_), tetapi dapat merusak konten non-ASCII. + +**PERINGATAN:** Menempelkan papan klip komputer ke perangkat (baik melalui +Ctrl+v or MOD+v) menyalin konten ke clipboard perangkat. Akibatnya, aplikasi Android apa pun dapat membaca kontennya. Anda harus menghindari menempelkan konten sensitif (seperti kata sandi) seperti itu. + + +#### Cubit untuk memperbesar/memperkecil + +Untuk mensimulasikan "cubit-untuk-memperbesar/memperkecil": Ctrl+_klik-dan-pindah_. + +Lebih tepatnya, tahan Ctrl sambil menekan tombol klik kiri. Hingga tombol klik kiri dilepaskan, semua gerakan mouse berskala dan memutar konten (jika didukung oleh aplikasi) relatif ke tengah layar. + +Secara konkret, scrcpy menghasilkan kejadian sentuh tambahan dari "jari virtual" di lokasi yang dibalik melalui bagian tengah layar. + + +#### Preferensi injeksi teks + +Ada dua jenis [peristiwa][textevents] dihasilkan saat mengetik teks: +- _peristiwa penting_, menandakan bahwa tombol ditekan atau dilepaskan; +- _peristiwa teks_, menandakan bahwa teks telah dimasukkan. + +Secara default, huruf dimasukkan menggunakan peristiwa kunci, sehingga keyboard berperilaku seperti yang diharapkan dalam game (biasanya untuk tombol WASD). + +Tapi ini mungkin [menyebabkan masalah][prefertext]. Jika Anda mengalami masalah seperti itu, Anda dapat menghindarinya dengan: + +```bash +scrcpy --prefer-text +``` + +(tapi ini akan merusak perilaku keyboard dalam game) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### Ulangi kunci + +Secara default, menahan tombol akan menghasilkan peristiwa kunci yang berulang. Ini dapat menyebabkan masalah kinerja di beberapa game, di mana acara ini tidak berguna. + +Untuk menghindari penerusan peristiwa penting yang berulang: + +```bash +scrcpy --no-key-repeat +``` + + +### Seret/jatuhkan file + +#### Pasang APK + +Untuk menginstal APK, seret & lepas file APK (diakhiri dengan `.apk`) ke jendela _scrcpy_. + +Tidak ada umpan balik visual, log dicetak ke konsol. + + +#### Dorong file ke perangkat + +Untuk mendorong file ke `/sdcard/` di perangkat, seret & jatuhkan file (non-APK) ke jendela _scrcpy_. + +Tidak ada umpan balik visual, log dicetak ke konsol. + +Direktori target dapat diubah saat mulai: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### Penerusan audio + +Audio tidak diteruskan oleh _scrcpy_. Gunakan [sndcpy]. + +Lihat juga [Masalah #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[Masalah #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Pintasan + +Dalam daftar berikut, MOD adalah pengubah pintasan. Secara default, ini (kiri) Alt atau (kiri) Super. + +Ini dapat diubah menggunakan `--shortcut-mod`. Kunci yang memungkinkan adalah `lctrl`,`rctrl`, `lalt`,` ralt`, `lsuper` dan` rsuper`. Sebagai contoh: + +```bash +# gunakan RCtrl untuk jalan pintas +scrcpy --shortcut-mod=rctrl + +# gunakan baik LCtrl+LAlt atau LSuper untuk jalan pintas +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] biasanya adalah Windows atau Cmd key._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Aksi | Pintasan + | ------------------------------------------------------|:----------------------------- + | Alihkan mode layar penuh | MOD+f + | Putar layar kiri | MOD+ _(kiri)_ + | Putar layar kanan | MOD+ _(kanan)_ + | Ubah ukuran jendela menjadi 1:1 (piksel-sempurna) | MOD+g + | Ubah ukuran jendela menjadi hapus batas hitam | MOD+w \| _klik-dua-kali¹_ + | Klik `HOME` | MOD+h \| _Klik-tengah_ + | Klik `BACK` | MOD+b \| _Klik-kanan²_ + | Klik `APP_SWITCH` | MOD+s + | Klik `MENU` (buka kunci layar) | MOD+m + | Klik `VOLUME_UP` | MOD+ _(naik)_ + | Klik `VOLUME_DOWN` | MOD+ _(turun)_ + | Klik `POWER` | MOD+p + | Menyalakan | _Klik-kanan²_ + | Matikan layar perangkat (tetap mirroring) | MOD+o + | Hidupkan layar perangkat | MOD+Shift+o + | Putar layar perangkat | MOD+r + | Luaskan panel notifikasi | MOD+n + | Ciutkan panel notifikasi | MOD+Shift+n + | Menyalin ke papan klip³ | MOD+c + | Potong ke papan klip³ | MOD+x + | Sinkronkan papan klip dan tempel³ | MOD+v + | Masukkan teks papan klip komputer | MOD+Shift+v + | Mengaktifkan/menonaktifkan penghitung FPS (di stdout) | MOD+i + | Cubit-untuk-memperbesar/memperkecil | Ctrl+_klik-dan-pindah_ + +_¹Klik-dua-kali pada batas hitam untuk menghapusnya._ +_²Klik-kanan akan menghidupkan layar jika mati, tekan BACK jika tidak._ +_³Hanya di Android >= 7._ + +Semua Ctrl+_key_ pintasan diteruskan ke perangkat, demikian adanya +ditangani oleh aplikasi aktif. + + +## Jalur kustom + +Untuk menggunakan biner _adb_ tertentu, konfigurasikan jalurnya di variabel lingkungan `ADB`: + + ADB=/path/to/adb scrcpy + +Untuk mengganti jalur file `scrcpy-server`, konfigurasikan jalurnya di +`SCRCPY_SERVER_PATH`. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## Mengapa _scrcpy_? + +Seorang kolega menantang saya untuk menemukan nama yang tidak dapat diucapkan seperti [gnirehtet]. + +[`strcpy`] menyalin sebuah **str**ing; `scrcpy` menyalin sebuah **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## Bagaimana Cara membangun? + +Lihat [BUILD]. + +[BUILD]: BUILD.md + + +## Masalah umum + +Lihat [FAQ](FAQ.md). + + +## Pengembang + +Baca [halaman pengembang]. + +[halaman pengembang]: DEVELOP.md + + +## Lisensi + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2020 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## Artikel + +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + diff --git a/README.md b/README.md index 2c5816a9..4c7fc0ec 100644 --- a/README.md +++ b/README.md @@ -755,6 +755,7 @@ Read the [developers page]. This README is available in other languages: +- [Indonesian (Indonesia, `id`)](README.id.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.12.1](README.pt-br.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.16](README.zh-Hans.md) From 25aff0093500c95c90192bc7e78f40ec3f617519 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 5 Oct 2020 21:22:54 +0200 Subject: [PATCH 214/214] Mention version of Indonesian translation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c7fc0ec..d10aaa74 100644 --- a/README.md +++ b/README.md @@ -755,7 +755,7 @@ Read the [developers page]. This README is available in other languages: -- [Indonesian (Indonesia, `id`)](README.id.md) +- [Indonesian (Indonesia, `id`) - v1.16](README.id.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.12.1](README.pt-br.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.16](README.zh-Hans.md)